Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: user card actions #658

Merged
merged 17 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
14 changes: 14 additions & 0 deletions backend/src/libs/guards/superAdmin.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';

@Injectable()
export class SuperAdminGuard implements CanActivate {
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();

try {
return request.user.isSAdmin;
} catch (error) {
throw new ForbiddenException();
}
}
RafaelSBatista97 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UpdateQuery } from 'mongoose';
import { QueryOptions, UpdateQuery } from 'mongoose';
import { ModelProps, SelectedValues } from '../types';

export interface BaseInterfaceRepository<T> {
Expand All @@ -14,5 +14,9 @@ export interface BaseInterfaceRepository<T> {

countDocuments(): Promise<number>;

findOneByFieldAndUpdate(value: ModelProps<T>, query: UpdateQuery<T>): Promise<T>;
findOneByFieldAndUpdate(
value: ModelProps<T>,
query: UpdateQuery<T>,
options?: QueryOptions<T>
): Promise<T>;
}
10 changes: 7 additions & 3 deletions backend/src/libs/repositories/mongo/mongo-generic.repository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Model, UpdateQuery } from 'mongoose';
import { Model, QueryOptions, UpdateQuery } from 'mongoose';
import { BaseInterfaceRepository } from '../interfaces/base.repository.interface';
import { ModelProps, SelectedValues } from '../types';

Expand Down Expand Up @@ -39,7 +39,11 @@ export class MongoGenericRepository<T> implements BaseInterfaceRepository<T> {
return this._repository.countDocuments().exec();
}

findOneByFieldAndUpdate(value: ModelProps<T>, query: UpdateQuery<T>): Promise<T> {
return this._repository.findOneAndUpdate(value, query).exec();
findOneByFieldAndUpdate(
value: ModelProps<T>,
query: UpdateQuery<T>,
options?: QueryOptions<T>
): Promise<T> {
return this._repository.findOneAndUpdate(value, query, options).exec();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import UpdateUserDto from '../dto/update.user.dto';
import { UpdateUserApplication } from '../interfaces/applications/update.user.service.interface';
import { UpdateUserService } from '../interfaces/services/update.user.service.interface';
import { TYPES } from '../interfaces/types';
Expand All @@ -21,4 +22,8 @@ export class UpdateUserApplicationImpl implements UpdateUserApplication {
checkEmail(token: string) {
return this.updateUserService.checkEmail(token);
}

updateSuperAdmin(user: UpdateUserDto) {
return this.updateUserService.updateSuperAdmin(user);
}
}
51 changes: 49 additions & 2 deletions backend/src/modules/users/controller/users.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
import { Body, Controller, Get, Inject, Put, UseGuards } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiBody,
ApiForbiddenResponse,
ApiInternalServerErrorResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
Expand All @@ -12,10 +15,16 @@ import JwtAuthenticationGuard from 'src/libs/guards/jwtAuth.guard';
import { BadRequestResponse } from 'src/libs/swagger/errors/bad-request.swagger';
import { InternalServerErrorResponse } from 'src/libs/swagger/errors/internal-server-error.swagger';
import { UnauthorizedResponse } from 'src/libs/swagger/errors/unauthorized.swagger';
import UpdateUserDto from '../dto/update.user.dto';
import UserDto from '../dto/user.dto';
import { GetUserApplication } from '../interfaces/applications/get.user.application.interface';
import { UpdateUserApplication } from '../interfaces/applications/update.user.service.interface';
import { TYPES } from '../interfaces/types';
import { UsersWithTeamsResponse } from '../swagger/users-with-teams.swagger';
import { SuperAdminGuard } from 'src/libs/guards/superAdmin.guard';
import { ForbiddenResponse } from '../../../libs/swagger/errors/forbidden.swagger';
import { NotFoundResponse } from '../../../libs/swagger/errors/not-found.swagger';
import { UpdateSuperAdminSwagger } from '../swagger/update.superadmin.swagger';

@ApiBearerAuth('access-token')
@ApiTags('Users')
Expand All @@ -24,7 +33,9 @@ import { UsersWithTeamsResponse } from '../swagger/users-with-teams.swagger';
export default class UsersController {
constructor(
@Inject(TYPES.applications.GetUserApplication)
private getUserApp: GetUserApplication
private getUserApp: GetUserApplication,
@Inject(TYPES.applications.UpdateUserApplication)
private updateUserApp: UpdateUserApplication
) {}

@ApiOperation({ summary: 'Retrieve a list of existing users' })
Expand Down Expand Up @@ -68,4 +79,40 @@ export default class UsersController {
getAllUsersWithTeams() {
return this.getUserApp.getUsersOnlyWithTeams();
}

@ApiOperation({ summary: 'Update user is super admin' })
@ApiBody({ type: UpdateSuperAdminSwagger })
@ApiOkResponse({
description: 'User successfully updated!',
type: UserDto
})
@ApiUnauthorizedResponse({
description: 'Unauthorized',
type: UnauthorizedResponse
})
@ApiBadRequestResponse({
description: 'Bad Request',
type: BadRequestResponse
})
@ApiInternalServerErrorResponse({
description: 'Internal Server Error',
type: InternalServerErrorResponse
})
@ApiNotFoundResponse({
type: NotFoundResponse,
description: 'Not found!'
})
@ApiForbiddenResponse({
description: 'Forbidden',
type: ForbiddenResponse
})
@ApiInternalServerErrorResponse({
description: 'Internal Server Error',
type: InternalServerErrorResponse
})
@UseGuards(SuperAdminGuard)
@Put('/sadmin')
async updateUserSuperAdmin(@Body() userData: UpdateUserDto) {
return await this.updateUserApp.updateSuperAdmin(userData);
}
RafaelSBatista97 marked this conversation as resolved.
Show resolved Hide resolved
}
30 changes: 30 additions & 0 deletions backend/src/modules/users/dto/update.user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsMongoId, IsNotEmpty, IsOptional, IsString } from 'class-validator';

export default class UpdateUserDto {
@ApiProperty()
@IsNotEmpty()
@IsMongoId()
@IsString()
@IsMongoId()
_id!: string;

@ApiProperty()
@IsOptional()
@IsString()
firstName?: string;

@ApiProperty()
@IsOptional()
@IsString()
lastName?: string;

@ApiProperty()
@IsOptional()
@IsString()
email?: string;

@ApiPropertyOptional({ default: false })
@IsOptional()
isSAdmin?: boolean;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LeanDocument } from 'mongoose';
import UpdateUserDto from '../../dto/update.user.dto';
import User, { UserDocument } from '../../entities/user.schema';

export interface UpdateUserApplication {
Expand All @@ -14,4 +15,6 @@ export interface UpdateUserApplication {
): Promise<User | null>;

checkEmail(token: string): Promise<string>;

updateSuperAdmin(user: UpdateUserDto): Promise<LeanDocument<UserDocument>>;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import User from '../../entities/user.schema';
import { LeanDocument } from 'mongoose';
import UpdateUserDto from '../../dto/update.user.dto';
import User, { UserDocument } from '../../entities/user.schema';

export interface UpdateUserService {
setCurrentRefreshToken(refreshToken: string, userId: string): Promise<User | null>;
Expand All @@ -10,4 +12,6 @@ export interface UpdateUserService {
): Promise<User | null>;

checkEmail(token: string): Promise<string>;

updateSuperAdmin(user: UpdateUserDto): Promise<LeanDocument<UserDocument>>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export interface UserRepositoryInterface extends BaseInterfaceRepository<User> {
getById(userId: string): Promise<User>;
updateUserWithRefreshToken(refreshToken: string, userId: string): Promise<User>;
updateUserPassword(email: string, password: string): Promise<User>;
updateSuperAdmin(userId: string, isSAdmin: boolean): Promise<User>;
}
4 changes: 4 additions & 0 deletions backend/src/modules/users/repository/user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ export class UserRepository
}
);
}

updateSuperAdmin(userId: string, isSAdmin: boolean) {
return this.findOneByFieldAndUpdate({ _id: userId }, { $set: { isSAdmin } }, { new: true });
}
}
11 changes: 11 additions & 0 deletions backend/src/modules/users/services/update.user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import { encrypt } from 'src/libs/utils/bcrypt';
import ResetPassword, {
ResetPasswordDocument
} from 'src/modules/auth/schemas/reset-password.schema';
import UpdateUserDto from '../dto/update.user.dto';
import { UpdateUserService } from '../interfaces/services/update.user.service.interface';
import { TYPES } from '../interfaces/types';
import { UserRepositoryInterface } from '../repository/user.repository.interface';
import User, { UserDocument } from '../entities/user.schema';

@Injectable()
export default class updateUserServiceImpl implements UpdateUserService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
@Inject(TYPES.repository)
private readonly userRepository: UserRepositoryInterface,
@InjectModel(ResetPassword.name)
Expand Down Expand Up @@ -55,4 +58,12 @@ export default class updateUserServiceImpl implements UpdateUserService {
throw new HttpException('EXPIRED_TOKEN', HttpStatus.BAD_REQUEST);
}
}

updateSuperAdmin(user: UpdateUserDto) {
const userToUpdate = this.userRepository.updateSuperAdmin(user._id, user.isSAdmin);

if (!userToUpdate) throw new HttpException('UPDATE_FAILED', HttpStatus.BAD_REQUEST);
RafaelSBatista97 marked this conversation as resolved.
Show resolved Hide resolved

return userToUpdate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';

export class UpdateSuperAdminSwagger {
@ApiProperty()
id!: string;

@ApiProperty()
isSAdmin!: boolean;
}
5 changes: 4 additions & 1 deletion frontend/src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import React from 'react';
import { render } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import Home from '../pages';

const queryClient = new QueryClient();

export const Wrapper: React.FC = ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
<QueryClientProvider client={queryClient}>
<RecoilRoot>{children}</RecoilRoot>
</QueryClientProvider>
);

jest.mock('next/config', () => () => ({
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/api/userService.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { GetServerSidePropsContext } from 'next';

import fetchData from '@/utils/fetchData';
import { User, UserWithTeams } from '../types/user/user';
import { User, UserWithTeams, UpdateUserIsAdmin } from '../types/user/user';

export const getAllUsers = (context?: GetServerSidePropsContext): Promise<User[]> =>
fetchData(`/users`, { context, serverSide: !!context });

export const getAllUsersWithTeams = (
context?: GetServerSidePropsContext,
): Promise<UserWithTeams[]> => fetchData(`/users/teams`, { context, serverSide: !!context });

export const updateUserIsAdminRequest = (user: UpdateUserIsAdmin): Promise<User> =>
fetchData(`/users/sadmin/`, { method: 'PUT', data: user });
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Icon from '@/components/icons/Icon';
import Flex from '@/components/Primitives/Flex';
import { Switch, SwitchThumb } from '@/components/Primitives/Switch';
import Text from '@/components/Primitives/Text';
import Tooltip from '@/components/Primitives/Tooltip';

type Props = {
title: string;
Expand All @@ -12,6 +13,8 @@ type Props = {
handleCheckedChange: (checked: boolean) => void;
children?: ReactNode;
color?: string;
disabled?: boolean;
styleVariant?: boolean;
};

const ConfigurationSettings = ({
Expand All @@ -21,22 +24,51 @@ const ConfigurationSettings = ({
handleCheckedChange,
children,
color,
disabled,
styleVariant,
}: Props) => (
<Flex gap={20}>
<Switch checked={isChecked} variant="sm" onCheckedChange={handleCheckedChange}>
<SwitchThumb variant="sm">
{isChecked && (
<Icon
name="check"
css={{
width: '$10',
height: '$10',
color: color || '$successBase',
}}
/>
)}
</SwitchThumb>
</Switch>
{styleVariant && (
<Tooltip content="Can't change your own role">
<Flex>
<Switch
checked={isChecked}
variant="disabled"
onCheckedChange={handleCheckedChange}
disabled={disabled}
>
<SwitchThumb variant="sm">
{isChecked && (
<Icon
name="check"
css={{
width: '$10',
height: '$10',
color: color || '$successBase',
}}
/>
)}
</SwitchThumb>
RafaelSBatista97 marked this conversation as resolved.
Show resolved Hide resolved
</Switch>
</Flex>
</Tooltip>
)}
{!styleVariant && (
<Switch checked={isChecked} variant="sm" onCheckedChange={handleCheckedChange}>
<SwitchThumb variant="sm">
{isChecked && (
<Icon
name="check"
css={{
width: '$10',
height: '$10',
color: color || '$successBase',
}}
/>
)}
</SwitchThumb>
</Switch>
)}
<Flex direction="column">
<Text size="md" weight="medium">
{title}
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/components/Primitives/Switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ const StyledSwitch = styled(SwitchPrimitive.Root, {
height: '$20',
},
md: { flex: '0 0 $sizes$42', width: '$42', height: '$24' },
disabled: {
'&[data-state="checked"]': { backgroundColor: '$successBase', opacity: 0.5 },
flex: '0 0 $sizes$35',
width: '$35',
height: '$20',
},
},
},
defaultVariants: {
Expand Down
Loading