Skip to content

Commit

Permalink
Merge b8f8712 into f56f8ba
Browse files Browse the repository at this point in the history
  • Loading branch information
brandstetterm authored Jul 19, 2024
2 parents f56f8ba + b8f8712 commit d789bd8
Show file tree
Hide file tree
Showing 39 changed files with 537 additions and 159 deletions.
116 changes: 95 additions & 21 deletions server/api.postman_collection.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"info": {
"_postman_id": "8a37e0fe-f06b-41e4-a538-6689616bd5a6",
"_postman_id": "45d5f690-f459-4371-a09f-8aff874591f2",
"name": "scrumlr.io",
"description": "This is the documentation for the REST API server of the application [scrumlr.io](https://scrumlr.io). You get in touch with us and send an email to [[email protected]](https://[email protected]). The software is [MIT licensed](https://opensource.org/licenses/MIT) so do whatever you want with it. If you want to checkout the progress of our development and take a peek into our backlog you can checkout our [GitHub repository](https://github.com/inovex/scrumlr.io). By the way, this already the third iteration of our server and we're still working on the interface and on further improvements. Since the API is mainly intended for our web client we won't start with API versions at the moment so breaking changes may be incoming. Once it got stable we'll maybe start with that.\n\nIf you're using the postman collection in order to explore the different resources you should also checkout the variables of the collection. Anytime you'll create new resources (e.g. your login or a board) variables will be stored and used for subsequent calls on other resources.\n\nAccess to protected resources will be authorized if a bearer token is sent or it is included in the `jwt` Cookie, which will be automatically set upon login.\n\n## Getting started\n\nLet's try to explain the basic flow of how a new board can will be created and someone tries to join the board as a participant.\n\nFirst you can check whether you are already logged in by a `GET` request on `/user`. See the _User_ section for more information.\n\n1. A user signs into the application (see _Login_ section)\n2. The user creates a new board (`POST` on `/boards`, checkout _Boards_ section)\n3. Another logged in user tries to join the board (`POST` on `/boards/{id}/participants`, checkout _Participants_ section)\n 1. If the boards access policy is set to `PUBLIC` the participant will be added to the board and afterwards all resources will be available\n 2. If the board requires a passphrase and the access policy is set to `BY_PASSPHRASE` a client error will be reported until the user sends the correct passphrase within the payload of the request\n 3. If the boards access policy is set to `BY_INVITE` a session request will be created instead and the user will be redirected to the new resource. The board owner now needs to accept or reject the request until the user can continue\n\nThese are just the basic steps of how sessions can be created. You can also have a look into the section _Realtime_ to see how you can open websockets and listen to live updates on the data.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "32423964"
"_exporter_id": "32837949"
},
"item": [
{
Expand Down Expand Up @@ -1962,15 +1962,6 @@
"originalRequest": {
"method": "GET",
"header": [],
"body": {
"mode": "raw",
"raw": "",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{base_url}}/boards",
"host": [
Expand All @@ -1981,8 +1972,8 @@
]
}
},
"_postman_previewlanguage": null,
"header": null,
"_postman_previewlanguage": "Text",
"header": [],
"cookie": [],
"body": "[\n {\n \"board\": {\n \"id\": \"4b08e0e9-f141-49b0-a75f-17d181f96969\",\n \"name\": \"My board\",\n \"description\": \"This is a test description\",\n \"accessPolicy\": \"PUBLIC\",\n \"showAuthors\": true,\n \"showNotesOfOtherUsers\": true,\n \"showNoteReactions\": true,\n \"allowStacking\": true,\n \"allowEditing\": true,\n \"sharedNote\": null,\n \"showVoting\": null\n },\n \"columnsNumber\": 2,\n \"createdAt\": \"0001-01-01T00:00:00Z\",\n \"participants\": 1\n }\n]"
}
Expand Down Expand Up @@ -2393,13 +2384,97 @@
" var allowStacking = pm.response.json().allowStacking;",
" pm.expect(allowStacking).to.eql(false);",
"})",
"",
""
],
"type": "text/javascript",
"packages": {}
}
}
],
"protocolProfileBehavior": {
"disabledSystemHeaders": {}
},
"request": {
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"sharedNote\": null,\n \"showVoting\": null,\n \"timerStart\": \"2024-01-18T13:38:49.304Z\",\n \"timerEnd\": \"2024-01-18T13:39:49.304Z\",\n \"showAuthors\": false,\n \"showNotesOfOtherUsers\": false,\n \"showNoteReactions\": false,\n \"allowStacking\": false\n // \"isLocked\": true // tested separately in next request\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{base_url}}/boards/{{board_id}}",
"host": [
"{{base_url}}"
],
"path": [
"boards",
"{{board_id}}"
]
}
},
"response": [
{
"name": "Success",
"originalRequest": {
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"sharedNote\":null,\n \"showVoting\":null,\n \"timerStart\":\"2024-01-18T13:38:49.304Z\",\n \"timerEnd\":\"2024-01-18T13:39:49.304Z\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{base_url}}/boards/{{board_id}}",
"host": [
"{{base_url}}"
],
"path": [
"boards",
"{{board_id}}"
]
}
},
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"description": "",
"type": "text"
}
],
"cookie": [],
"body": "{\n \"id\": \"{{board_id}}\",\n \"name\": \"My board\",\n \"description\": \"Updated to new description\",\n \"accessPolicy\": \"BY_PASSPHRASE\",\n \"showAuthors\": false,\n \"showNotesOfOtherUsers\": false,\n \"showNoteReactions\": false,\n \"allowStacking\": false,\n \"isLocked\": false,\n \"timerStart\": \"2024-01-18T13:38:49.304Z\",\n \"timerEnd\": \"2024-01-18T13:39:49.304Z\",\n \"sharedNote\": null,\n \"showVoting\": null\n}"
}
]
},
{
"name": "Lock Board",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Successful PUT request\", function () {",
" pm.expect(pm.response).to.have.status(200);",
"});",
"",
"pm.test(\"Check allowEditing changed\", function () {",
" var allowEditing = pm.response.json().allowEditing;",
" pm.expect(allowEditing).to.eql(false);",
" var isLocked = pm.response.json().isLocked;",
" pm.expect(isLocked).to.eql(true);",
"})",
""
"",
"// one could add a test which checks whether the lock actually works, ",
"// but you'd need to change your role from moderator to normal participant or add another participant to verify"
],
"type": "text/javascript",
"packages": {}
Expand All @@ -2414,7 +2489,7 @@
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"sharedNote\": null,\n \"showVoting\": null,\n \"timerStart\": \"2024-01-18T13:38:49.304Z\",\n \"timerEnd\": \"2024-01-18T13:39:49.304Z\",\n \"showAuthors\": false,\n \"showNotesOfOtherUsers\": false,\n \"showNoteReactions\": false,\n \"allowStacking\": false,\n \"allowEditing\": false\n}",
"raw": "{\n \"isLocked\": true\n}",
"options": {
"raw": {
"language": "json"
Expand All @@ -2440,7 +2515,7 @@
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"sharedNote\":null,\n \"showVoting\":null,\n \"timerStart\":\"2024-01-18T13:38:49.304Z\",\n \"timerEnd\":\"2024-01-18T13:39:49.304Z\",\n \"allowEditing\":true\n}",
"raw": "{\n \"isLocked\": true\n}",
"options": {
"raw": {
"language": "json"
Expand All @@ -2463,13 +2538,12 @@
{
"key": "Content-Type",
"value": "application/json",
"name": "Content-Type",
"description": "",
"type": "text"
}
],
"cookie": [],
"body": "{\n \"id\": \"{{board_id}}\",\n \"name\": \"My board\",\n \"description\": \"Updated to new description\",\n \"accessPolicy\": \"BY_PASSPHRASE\",\n \"showAuthors\": false,\n \"showNotesOfOtherUsers\": false,\n \"showNoteReactions\": false,\n \"allowStacking\": false,\n \"allowEditing\": false,\n \"timerStart\": \"2024-01-18T13:38:49.304Z\",\n \"timerEnd\": \"2024-01-18T13:39:49.304Z\",\n \"sharedNote\": null,\n \"showVoting\": null\n}"
"body": "{\n \"id\": \"{{board_id}}\",\n \"name\": \"My board\",\n \"description\": \"Updated to new description\",\n \"accessPolicy\": \"BY_PASSPHRASE\",\n \"showAuthors\": false,\n \"showNotesOfOtherUsers\": false,\n \"showNoteReactions\": false,\n \"isLocked\": true,\n \"allowEditing\": false,\n \"timerStart\": \"2024-01-18T13:38:49.304Z\",\n \"timerEnd\": \"2024-01-18T13:39:49.304Z\",\n \"sharedNote\": null,\n \"showVoting\": null\n}"
}
]
}
Expand Down
4 changes: 2 additions & 2 deletions server/src/api/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,13 @@ func (s *Server) BoardEditableContext(next http.Handler) http.Handler {
return
}

if !isMod && !settings.AllowEditing {
if !isMod && settings.IsLocked {
log.Errorw("not allowed to edit board", "err", err)
common.Throw(w, r, common.ForbiddenError(errors.New("not authorized to change board")))
return
}

boardEditable := context.WithValue(r.Context(), identifiers.BoardEditableIdentifier, settings.AllowEditing)
boardEditable := context.WithValue(r.Context(), identifiers.BoardEditableIdentifier, settings.IsLocked)
next.ServeHTTP(w, r.WithContext(boardEditable))
})
}
Expand Down
16 changes: 8 additions & 8 deletions server/src/api/notes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,12 +254,12 @@ func (suite *NotesTestSuite) TestDeleteNote() {
name string
expectedCode int
err error
allowEditing bool
isLocked bool
}{
{
name: "Delete Note when board is unlocked",
expectedCode: http.StatusNoContent,
allowEditing: true,
isLocked: true,
},
{
name: "Delete Note when board is locked",
Expand All @@ -270,7 +270,7 @@ func (suite *NotesTestSuite) TestDeleteNote() {
StatusText: "Bad request",
ErrorText: "something",
},
allowEditing: false,
isLocked: false,
},
}
for _, tt := range tests {
Expand All @@ -289,8 +289,8 @@ func (suite *NotesTestSuite) TestDeleteNote() {
r := chi.NewRouter()
s.initNoteResources(r)
boardMock.On("Get", boardID).Return(&dto.Board{
ID: boardID,
AllowEditing: tt.allowEditing,
ID: boardID,
IsLocked: tt.isLocked,
}, nil)

// Mock the SessionExists method
Expand All @@ -302,12 +302,12 @@ func (suite *NotesTestSuite) TestDeleteNote() {
// Mock the ParticipantBanned method
sessionMock.On("ParticipantBanned", mock.Anything, boardID, userID).Return(false, nil)

if tt.allowEditing {
if tt.isLocked {
noteMock.On("Delete", mock.Anything, mock.Anything).Return(nil)
} else {
boardMock.On("Get", boardID).Return(&dto.Board{
ID: boardID,
AllowEditing: tt.allowEditing,
ID: boardID,
IsLocked: tt.isLocked,
}, tt.err)
noteMock.On("Delete", mock.Anything, mock.Anything).Return(tt.err)
}
Expand Down
1 change: 0 additions & 1 deletion server/src/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,6 @@ func (s *Server) initReactionResources(r chi.Router) {
func (s *Server) initBoardReactionResources(r chi.Router) {
r.Route("/board-reactions", func(r chi.Router) {
r.Use(s.BoardParticipantContext)
r.Use(s.BoardEditableContext)

r.Post("/", s.createBoardReaction)
})
Expand Down
6 changes: 3 additions & 3 deletions server/src/common/dto/boards.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type Board struct {

AllowStacking bool `json:"allowStacking"`

AllowEditing bool `json:"allowEditing"`
IsLocked bool `json:"isLocked"`

TimerStart *time.Time `json:"timerStart,omitempty"`
TimerEnd *time.Time `json:"timerEnd,omitempty"`
Expand All @@ -57,7 +57,7 @@ func (b *Board) From(board database.Board) *Board {
b.ShowNotesOfOtherUsers = board.ShowNotesOfOtherUsers
b.ShowNoteReactions = board.ShowNoteReactions
b.AllowStacking = board.AllowStacking
b.AllowEditing = board.AllowEditing
b.IsLocked = board.IsLocked
b.SharedNote = board.SharedNote
b.ShowVoting = board.ShowVoting
b.TimerStart = board.TimerStart
Expand Down Expand Up @@ -121,7 +121,7 @@ type BoardUpdateRequest struct {
AllowStacking *bool `json:"allowStacking"`

// Set whether changes to board should be allowed to all users or only moderators.
AllowEditing *bool `json:"allowEditing"`
IsLocked *bool `json:"isLocked"`

// Set the timer start.
TimerStart *time.Time `json:"timerStart"`
Expand Down
8 changes: 4 additions & 4 deletions server/src/database/boards.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type Board struct {
ShowNotesOfOtherUsers bool
ShowNoteReactions bool
AllowStacking bool
AllowEditing bool
IsLocked bool
CreatedAt time.Time
TimerStart *time.Time
TimerEnd *time.Time
Expand Down Expand Up @@ -60,7 +60,7 @@ type BoardUpdate struct {
ShowNotesOfOtherUsers *bool
ShowNoteReactions *bool
AllowStacking *bool
AllowEditing *bool
IsLocked *bool
TimerStart *time.Time
TimerEnd *time.Time
SharedNote uuid.NullUUID
Expand Down Expand Up @@ -143,8 +143,8 @@ func (d *Database) UpdateBoard(update BoardUpdate) (Board, error) {
if update.AllowStacking != nil {
query.Column("allow_stacking")
}
if update.AllowEditing != nil {
query.Column("allow_editing")
if update.IsLocked != nil {
query.Column("is_locked")
}

var board Board
Expand Down
6 changes: 3 additions & 3 deletions server/src/database/boards_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,13 +442,13 @@ func testUpdateBoardSettings(t *testing.T) {

showAuthors := true
showNotesOfOtherUsers := true
allowEditing := true
updatedBoard, err := testDb.UpdateBoard(BoardUpdate{ID: board.ID, ShowAuthors: &showAuthors, ShowNotesOfOtherUsers: &showNotesOfOtherUsers, AllowEditing: &allowEditing})
isLocked := true
updatedBoard, err := testDb.UpdateBoard(BoardUpdate{ID: board.ID, ShowAuthors: &showAuthors, ShowNotesOfOtherUsers: &showNotesOfOtherUsers, IsLocked: &isLocked})

assert.Nil(t, err)
assert.Equal(t, showAuthors, updatedBoard.ShowAuthors)
assert.Equal(t, showNotesOfOtherUsers, updatedBoard.ShowNotesOfOtherUsers)
assert.Equal(t, allowEditing, updatedBoard.AllowEditing)
assert.Equal(t, isLocked, updatedBoard.IsLocked)
}

func testGetBoard(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE boards ALTER COLUMN is_locked SET DEFAULT TRUE;
ALTER TABLE boards RENAME COLUMN is_locked TO allow_editing;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE boards ALTER COLUMN allow_editing SET DEFAULT FALSE;
ALTER TABLE boards RENAME COLUMN allow_editing TO is_locked;
2 changes: 1 addition & 1 deletion server/src/services/boards/boards.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ func (s *BoardService) Update(ctx context.Context, body dto.BoardUpdateRequest)
ShowNotesOfOtherUsers: body.ShowNotesOfOtherUsers,
ShowNoteReactions: body.ShowNoteReactions,
AllowStacking: body.AllowStacking,
AllowEditing: body.AllowEditing,
IsLocked: body.IsLocked,
TimerStart: body.TimerStart,
TimerEnd: body.TimerEnd,
SharedNote: body.SharedNote,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import "./AccessPolicySelection.scss";
import {AccessPolicy} from "types/board";
import {generateRandomString} from "utils/random";
import {useTranslation} from "react-i18next";
import {Visible, Hidden, Duplicate,Refresh} from "components/Icon";
import {Visible, Hidden, Duplicate, Refresh} from "components/Icon";
import {TextInputAdornment} from "components/TextInputAdornment";
import {TextInputLabel} from "../TextInputLabel";
import {TextInput} from "../TextInput";
Expand Down
12 changes: 11 additions & 1 deletion src/components/Board/Board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import "./Board.scss";
import {useDndMonitor} from "@dnd-kit/core";
import classNames from "classnames";
import {useStripeOffset} from "utils/hooks/useStripeOffset";
import {Toast} from "utils/Toast";
import {useTranslation} from "react-i18next";

export interface BoardProps {
children: React.ReactElement<ColumnProps> | React.ReactElement<ColumnProps>[];
currentUserIsModerator: boolean;
moderating: boolean;
locked: boolean;
}

export interface BoardState {
Expand All @@ -26,7 +29,8 @@ export interface ColumnState {
lastVisibleColumnIndex: number;
}

export const BoardComponent = ({children, currentUserIsModerator, moderating}: BoardProps) => {
export const BoardComponent = ({children, currentUserIsModerator, moderating, locked}: BoardProps) => {
const {t} = useTranslation();
const [state, setState] = useState<BoardState & ColumnState>({
firstVisibleColumnIndex: 0,
lastVisibleColumnIndex: React.Children.count(children),
Expand Down Expand Up @@ -135,6 +139,12 @@ export const BoardComponent = ({children, currentUserIsModerator, moderating}: B
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [columnState]);

useEffect(() => {
if (locked) {
Toast.info({title: t("Toast.lockedBoard"), autoClose: 10_000});
}
}, [t, locked]);

if (!children || columnsCount === 0) {
// Empty board
return (
Expand Down
Loading

0 comments on commit d789bd8

Please sign in to comment.