Skip to content

Commit

Permalink
[FEATURE]: Board Settings Permissions (#345)
Browse files Browse the repository at this point in the history
Co-authored-by: Catia Barroco <104831678+CatiaBarroco-xgeeks@users.noreply.github.com>
Co-authored-by: Francisco Morgado <f.morgado@kigroup.de>
Co-authored-by: Daniel Sousa @tutods <jdaniel.asousa@gmail.com>
Co-authored-by: Daniel Sousa <tutods@Macbook-Pro.local>
  • Loading branch information
5 people authored Jul 22, 2022
1 parent 3a6b673 commit d92470a
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 60 deletions.
3 changes: 2 additions & 1 deletion backend/src/modules/boards/controller/boards.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ export default class BoardsController {

@UseGuards(JwtAuthenticationGuard)
@Get()
getAllBoards(@Req() request: RequestWithUser, @Query() { page, size }: PaginationParams) {
async getAllBoards(@Req() request: RequestWithUser, @Query() { page, size }: PaginationParams) {
const { _id: userId, isSAdmin } = request.user;
if (isSAdmin) {
return this.getBoardApp.getSuperAdminBoards(userId, page, size);
}

return this.getBoardApp.getUsersBoards(userId, page, size);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LeanDocument } from 'mongoose';
import { Document, LeanDocument } from 'mongoose';

import { BoardDocument } from '../../schemas/board.schema';
import Board, { BoardDocument } from '../../schemas/board.schema';
import { BoardsAndPage } from '../boards-page.interface';

export interface GetBoardServiceInterface {
Expand Down Expand Up @@ -29,5 +29,9 @@ export interface GetBoardServiceInterface {
| null
>;

getMainBoardData(
boardId: string
): Promise<LeanDocument<Board & Document<any, any, any> & { _id: any }> | null>;

countBoards(userId: string): Promise<number>;
}
10 changes: 8 additions & 2 deletions backend/src/modules/boards/services/create.board.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,17 @@ export default class CreateBoardServiceImpl implements CreateBoardService {
}

async createBoard(boardData: BoardDto, userId: string, isSubBoard = false) {
const { dividedBoards = [] } = boardData;
const { dividedBoards = [], team } = boardData;

/**
* Add in each divided board the team id (from main board)
*/
const dividedBoardsWithTeam = dividedBoards.map((dividedBoard) => ({ ...dividedBoard, team }));

return this.boardModel.create({
...boardData,
createdBy: userId,
dividedBoards: await this.createDividedBoards(dividedBoards, userId),
dividedBoards: await this.createDividedBoards(dividedBoardsWithTeam, userId),
isSubBoard
});
}
Expand Down
32 changes: 23 additions & 9 deletions backend/src/modules/boards/services/get.board.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,22 +102,36 @@ export default class GetBoardServiceImpl implements GetBoardServiceInterface {
select: 'user role',
populate: {
path: 'user',
select: 'firstName lastName joinedAt'
select: '_id firstName lastName joinedAt'
}
}
})
.populate({
path: 'dividedBoards',
select: '-__v -createdAt -id',
populate: {
path: 'users',
select: 'role user',
populate: {
path: 'user',
model: 'User',
select: 'firstName lastName joinedAt'
populate: [
{
path: 'users',
select: 'role user',
populate: {
path: 'user',
model: 'User',
select: 'firstName lastName joinedAt'
}
},
{
path: 'team',
select: 'name users _id',
populate: {
path: 'users',
select: 'user role',
populate: {
path: 'user',
select: '_id firstName lastName joinedAt'
}
}
}
}
]
})
.populate({
path: 'users',
Expand Down
116 changes: 93 additions & 23 deletions backend/src/modules/boards/services/update.board.service.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,115 @@
import { Inject, Injectable } from '@nestjs/common';
import { ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { LeanDocument, Model, ObjectId } from 'mongoose';

import { TeamRoles } from 'libs/enum/team.roles';
import { GetTeamServiceInterface } from 'modules/teams/interfaces/services/get.team.service.interface';
import * as Teams from 'modules/teams/interfaces/types';
import { TeamUserDocument } from 'modules/teams/schemas/team.user.schema';

import { UpdateBoardDto } from '../dto/update-board.dto';
import { UpdateBoardService } from '../interfaces/services/update.board.service.interface';
import Board, { BoardDocument } from '../schemas/board.schema';
import BoardUser, { BoardUserDocument } from '../schemas/board.user.schema';

@Injectable()
export default class UpdateBoardServiceImpl implements UpdateBoardService {
constructor(
@InjectModel(Board.name) private boardModel: Model<BoardDocument>,
@Inject(Teams.TYPES.services.GetTeamService)
private getTeamService: GetTeamServiceInterface
private getTeamService: GetTeamServiceInterface,
@InjectModel(BoardUser.name)
private boardUserModel: Model<BoardUserDocument>
) {}

/**
* Method to retrieve user details from team.
* This method is used to see if the user is Admin or a Stakeholder
*
* @param userId Current User Logged
* @param teamId Team ID (team from board)
* @returns Team User
*/
private async getTeamUser(
userId: string,
teamId: string
): Promise<LeanDocument<TeamUserDocument>> {
const teamUser = await this.getTeamService.getTeamUser(userId, teamId);

if (!teamUser) {
throw new NotFoundException('User not found on this team!');
}

return teamUser;
}

/**
* Method to get user from board, if it's responsible
* If not, return a null value
*
* @param userId Current User Logged
* @returns Board User
*/
private async getResponsible(userId: string): Promise<LeanDocument<BoardUserDocument> | null> {
const user = await this.boardUserModel.findOne({ user: userId }).lean().exec();

if (!user) {
throw new NotFoundException('User not found!');
}

if (user.role !== 'responsible') {
return null;
}

return user;
}

async update(userId: string, boardId: string, boardData: UpdateBoardDto) {
const currentVotes = await this.boardModel.findById(boardId, 'maxVotes totalUsedVotes').exec();
const board = await this.boardModel.findById(boardId).exec();

return this.boardModel
.findOneAndUpdate(
{
_id: boardId,
createdBy: userId
},
{
...boardData,
maxVotes:
Number(boardData.maxVotes) < Number(currentVotes?.maxVotes) &&
currentVotes?.totalUsedVotes !== 0
? currentVotes?.maxVotes
: boardData.maxVotes
},
{
new: true
}
)
.lean()
.exec();
if (!board) {
throw new NotFoundException('Board not found!');
}

// Destructuring board variables
const { isSubBoard, team, createdBy } = board;

// Get Team User to see if is Admin or Stakeholder
const teamUser = await this.getTeamUser(userId, String(team));

// Role Validation
const isAdminOrStakeholder = [TeamRoles.STAKEHOLDER, TeamRoles.ADMIN].includes(
teamUser.role as TeamRoles
);

// Get user info to see if is responsible or not
const subBoardResponsible = await this.getResponsible(userId);

// Validate if the logged user are the owner
const isOwner = String(userId) === String(createdBy);

if (isAdminOrStakeholder || isOwner || (isSubBoard && !!subBoardResponsible)) {
return this.boardModel
.findOneAndUpdate(
{
_id: boardId
},
{
...boardData,
maxVotes:
Number(boardData.maxVotes) < Number(board?.maxVotes) && board?.totalUsedVotes !== 0
? board?.maxVotes
: boardData.maxVotes
},
{
new: true
}
)
.lean()
.exec();
}

throw new ForbiddenException('You are not allowed to update this board!');
}

async mergeBoards(subBoardId: string, userId: string) {
Expand Down
28 changes: 27 additions & 1 deletion frontend/src/components/Board/Settings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Dispatch, ReactNode, SetStateAction, useEffect, useState } from 'react';
import React, { Dispatch, ReactNode, SetStateAction, useEffect, useRef, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useRecoilState, useRecoilValue } from 'recoil';
import { joiResolver } from '@hookform/resolvers/joi';
Expand Down Expand Up @@ -38,6 +38,8 @@ type Props = {
};

const BoardSettings = ({ isOpen, setIsOpen, socketId }: Props) => {
const submitBtnRef = useRef<HTMLButtonElement | null>(null);

const [updateBoardData, setUpdateBoardData] = useRecoilState(updateBoardDataState);
const haveError = useRecoilValue(updateBoardError);

Expand Down Expand Up @@ -169,6 +171,29 @@ const BoardSettings = ({ isOpen, setIsOpen, socketId }: Props) => {
);
};

/**
* Use Effect to submit the board settings form when press enter key
* (Note: Radix Dialog close when pressing enter)
*/
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault();

if (submitBtnRef.current) {
submitBtnRef.current.click();
}
}
};

document.addEventListener('keydown', keyDownHandler);

return () => {
document.removeEventListener('keydown', keyDownHandler);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const configurationSettings = (
title: string,
text: string,
Expand Down Expand Up @@ -309,6 +334,7 @@ const BoardSettings = ({ isOpen, setIsOpen, socketId }: Props) => {
Cancel
</Button>
<Button
ref={submitBtnRef}
type="submit"
variant="primary"
css={{ marginRight: '$32', padding: '$16 $24' }}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/CardBoard/CardBody/CardEnd.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const CardEnd: React.FC<CardEndProps> = React.memo(
Responsible
</Text>
<CardAvatars
listUsers={!team ? users : team.users}
listUsers={users}
responsible
teamAdmins={false}
userId={userId}
Expand Down
Loading

0 comments on commit d92470a

Please sign in to comment.