From 4559318e7615982b99b4313a5f66cd596003ac49 Mon Sep 17 00:00:00 2001 From: Pavlo Strunkin Date: Sat, 24 Jul 2021 17:03:45 +0300 Subject: [PATCH 1/5] Add roles https://github.com/Visual-Regression-Tracker/Visual-Regression-Tracker/issues/144 --- .../migrations/20210724121852-roles/README.md | 52 ++++++ .../20210724121852-roles/schema.prisma | 150 ++++++++++++++++++ .../20210724121852-roles/steps.json | 46 ++++++ prisma/migrations/migrate.lock | 1 + prisma/schema.prisma | 7 + prisma/seed.ts | 57 ++++--- src/auth/guards/role.guard.ts | 47 ++++++ src/builds/builds.controller.ts | 18 ++- src/projects/projects.controller.ts | 13 +- src/shared/roles.decorator.ts | 4 + src/test-runs/test-runs.controller.ts | 28 ++-- .../test-variations.controller.ts | 10 +- 12 files changed, 387 insertions(+), 46 deletions(-) create mode 100644 prisma/migrations/20210724121852-roles/README.md create mode 100644 prisma/migrations/20210724121852-roles/schema.prisma create mode 100644 prisma/migrations/20210724121852-roles/steps.json create mode 100644 src/auth/guards/role.guard.ts create mode 100644 src/shared/roles.decorator.ts diff --git a/prisma/migrations/20210724121852-roles/README.md b/prisma/migrations/20210724121852-roles/README.md new file mode 100644 index 00000000..e6f90be8 --- /dev/null +++ b/prisma/migrations/20210724121852-roles/README.md @@ -0,0 +1,52 @@ +# Migration `20210724121852-roles` + +This migration has been generated by Pavlo Strunkin at 7/24/2021, 3:18:52 PM. +You can check out the [state of the schema](./schema.prisma) after the migration. + +## Database Steps + +```sql +CREATE TYPE "public"."Role" AS ENUM ('admin', 'editor', 'guest') + +ALTER TABLE "User" ADD COLUMN "role" "Role" NOT NULL DEFAULT E'guest' +``` + +## Changes + +```diff +diff --git schema.prisma schema.prisma +migration 20210709133029-275-project-to-testrun-relation..20210724121852-roles +--- datamodel.dml ++++ datamodel.dml +@@ -3,9 +3,9 @@ + } + datasource db { + provider = "postgresql" +- url = "***" ++ url = "***" + } + model Build { + id String @id @default(uuid()) +@@ -122,8 +122,9 @@ + apiKey String @unique + isActive Boolean @default(true) + builds Build[] + baselines Baseline[] ++ role Role @default(guest) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + } +@@ -140,4 +141,10 @@ + pixelmatch + lookSame + odiff + } ++ ++enum Role { ++ admin ++ editor ++ guest ++} +``` + + diff --git a/prisma/migrations/20210724121852-roles/schema.prisma b/prisma/migrations/20210724121852-roles/schema.prisma new file mode 100644 index 00000000..1e96e1e6 --- /dev/null +++ b/prisma/migrations/20210724121852-roles/schema.prisma @@ -0,0 +1,150 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = "***" +} + +model Build { + id String @id @default(uuid()) + ciBuildId String? + number Int? + branchName String? + status String? + testRuns TestRun[] + projectId String + project Project @relation(fields: [projectId], references: [id]) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + user User? @relation(fields: [userId], references: [id]) + userId String? + isRunning Boolean? + + @@unique([projectId, ciBuildId]) +} + +model Project { + id String @id @default(uuid()) + name String + mainBranchName String @default("master") + builds Build[] + buildsCounter Int @default(0) + maxBuildAllowed Int @default(100) + maxBranchLifetime Int @default(30) + testVariations TestVariation[] + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + // config + autoApproveFeature Boolean @default(false) + imageComparison ImageComparison @default(pixelmatch) + imageComparisonConfig String @default("{ \"threshold\": 0.1, \"ignoreAntialiasing\": true, \"allowDiffDimensions\": false }") + + TestRun TestRun[] + @@unique([name]) +} + +model TestRun { + id String @id @default(uuid()) + imageName String + diffName String? + diffPercent Float? + diffTollerancePercent Float @default(0) + pixelMisMatchCount Int? + status TestStatus + buildId String + build Build @relation(fields: [buildId], references: [id]) + testVariationId String? + testVariation TestVariation? @relation(fields: [testVariationId], references: [id]) + projectId String? + project Project? @relation(fields: [projectId], references: [id]) + merge Boolean @default(false) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + // Test variation data + name String @default("") + browser String? + device String? + os String? + viewport String? + customTags String? @default("") + baselineName String? + comment String? + baseline Baseline? + branchName String @default("master") + baselineBranchName String? + ignoreAreas String @default("[]") + tempIgnoreAreas String @default("[]") +} + +model TestVariation { + id String @id @default(uuid()) + name String + branchName String @default("master") + browser String @default("") + device String @default("") + os String @default("") + viewport String @default("") + customTags String @default("") + baselineName String? + ignoreAreas String @default("[]") + projectId String + project Project @relation(fields: [projectId], references: [id]) + testRuns TestRun[] + baselines Baseline[] + comment String? + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + + @@unique([projectId, name, browser, device, os, viewport, customTags, branchName]) +} + +model Baseline { + id String @id @default(uuid()) + baselineName String + testVariationId String + testVariation TestVariation @relation(fields: [testVariationId], references: [id]) + testRunId String? + testRun TestRun? @relation(fields: [testRunId], references: [id]) + userId String? + user User? @relation(fields: [userId], references: [id]) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) +} + +model User { + id String @id @default(uuid()) + email String @unique + password String + firstName String? + lastName String? + apiKey String @unique + isActive Boolean @default(true) + builds Build[] + baselines Baseline[] + role Role @default(guest) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) +} + +enum TestStatus { + failed + new + ok + unresolved + approved + autoApproved +} + +enum ImageComparison { + pixelmatch + lookSame + odiff +} + +enum Role { + admin + editor + guest +} diff --git a/prisma/migrations/20210724121852-roles/steps.json b/prisma/migrations/20210724121852-roles/steps.json new file mode 100644 index 00000000..b8fa6233 --- /dev/null +++ b/prisma/migrations/20210724121852-roles/steps.json @@ -0,0 +1,46 @@ +{ + "version": "0.3.14-fixed", + "steps": [ + { + "tag": "CreateEnum", + "enum": "Role", + "values": [ + "admin", + "editor", + "guest" + ] + }, + { + "tag": "CreateField", + "model": "User", + "field": "role", + "type": "Role", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "User", + "field": "role" + }, + "directive": "default" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "User", + "field": "role" + }, + "directive": "default" + }, + "argument": "", + "value": "guest" + } + ] +} \ No newline at end of file diff --git a/prisma/migrations/migrate.lock b/prisma/migrations/migrate.lock index 73c4928c..98d9703d 100644 --- a/prisma/migrations/migrate.lock +++ b/prisma/migrations/migrate.lock @@ -23,3 +23,4 @@ 20210705154453-baseline-author 20210709115233-gh-275-max-branch-lifetime 20210709133029-275-project-to-testrun-relation +20210724121852-roles \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dd691e92..72da9e09 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -123,6 +123,7 @@ model User { isActive Boolean @default(true) builds Build[] baselines Baseline[] + role Role @default(guest) updatedAt DateTime @updatedAt createdAt DateTime @default(now()) } @@ -141,3 +142,9 @@ enum ImageComparison { lookSame odiff } + +enum Role { + admin + editor + guest +} diff --git a/prisma/seed.ts b/prisma/seed.ts index 158e3091..ac1081c5 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,4 +1,4 @@ -import { PrismaClient } from '@prisma/client'; +import { PrismaClient, Role } from '@prisma/client'; import uuidAPIKey from 'uuid-apikey'; import { genSalt, hash } from 'bcryptjs'; @@ -20,29 +20,38 @@ seed() async function createDefaultUser() { const userList = await prisma.user.findMany(); console.log(userList); - if (userList.length === 0) { - const defaultEmail = 'visual-regression-tracker@example.com'; - const defaultPassword = '123456'; - const salt = await genSalt(10); - await prisma.user - .create({ - data: { - email: defaultEmail, - firstName: 'fname', - lastName: 'lname', - apiKey: uuidAPIKey.create({ noDashes: true }).apiKey, - password: await hash(defaultPassword, salt), - }, - }) - .then((user) => { - console.log('###########################'); - console.log('## CREATING DEFAULT USER ##'); - console.log('###########################'); - console.log(''); - console.log(`The user with the email "${defaultEmail}" and password "${defaultPassword}" was created`); - console.log(`The Api key is: ${user.apiKey}`); - }); - } + + const defaultEmail = 'visual-regression-tracker@example.com'; + const defaultPassword = '123456'; + const salt = await genSalt(10); + + await prisma.user + .upsert({ + where: { + email: defaultEmail, + }, + update: { + role: Role.admin, + }, + create: { + email: defaultEmail, + firstName: 'fname', + lastName: 'lname', + role: Role.admin, + apiKey: uuidAPIKey.create({ noDashes: true }).apiKey, + password: await hash(defaultPassword, salt), + }, + }) + .then((user) => { + console.log('###########################'); + console.log('####### DEFAULT USER ######'); + console.log('###########################'); + console.log(''); + console.log( + `The user with the email "${defaultEmail}" and password "${defaultPassword}" was created (if not changed before)` + ); + console.log(`The Api key is: ${user.apiKey}`); + }); } async function createDefaultProject() { diff --git a/src/auth/guards/role.guard.ts b/src/auth/guards/role.guard.ts new file mode 100644 index 00000000..62a04d08 --- /dev/null +++ b/src/auth/guards/role.guard.ts @@ -0,0 +1,47 @@ +import { ExecutionContext, Injectable, CanActivate, UnauthorizedException } from '@nestjs/common'; +import { Role, User } from '@prisma/client'; +import { Request } from 'express'; +import { PrismaService } from '../../prisma/prisma.service'; +import { Reflector } from '@nestjs/core'; + +@Injectable() +export class RoleGuard implements CanActivate { + constructor(private readonly prismaService: PrismaService, private reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const roles = this.reflector.get('roles', context.getHandler()); + if (!roles) { + return true; + } + + const user = await this.getUser(context); + return this.checkPermission(user); + } + + checkPermission = (user: User): boolean => { + console.log(user); + switch (user.role) { + case Role.admin: { + return true; + } + case Role.editor: { + // check project permissions later + return true; + } + default: + return false; + } + }; + + getUser = async (context: ExecutionContext): Promise => { + const request: Request = context.switchToHttp().getRequest(); + + if (request.user) { + return request.user as User; + } + + return this.prismaService.user.findUnique({ + where: { apiKey: request.header('apiKey') }, + }); + }; +} diff --git a/src/builds/builds.controller.ts b/src/builds/builds.controller.ts index 30e27060..b3fa9e5a 100644 --- a/src/builds/builds.controller.ts +++ b/src/builds/builds.controller.ts @@ -19,13 +19,15 @@ import { JwtAuthGuard } from '../auth/guards/auth.guard'; import { ApiBearerAuth, ApiTags, ApiSecurity, ApiOkResponse } from '@nestjs/swagger'; import { CreateBuildDto } from './dto/build-create.dto'; import { ApiGuard } from '../auth/guards/api.guard'; -import { Build } from '@prisma/client'; +import { Build, Role } from '@prisma/client'; import { BuildDto } from './dto/build.dto'; import { MixedGuard } from '../auth/guards/mixed.guard'; import { PaginatedBuildDto } from './dto/build-paginated.dto'; import { ModifyBuildDto } from './dto/build-modify.dto'; import { ProjectsService } from '../projects/projects.service'; import { EventsGateway } from '../shared/events/events.gateway'; +import { RoleGuard } from 'src/auth/guards/role.guard'; +import { Roles } from 'src/shared/roles.decorator'; @Controller('builds') @ApiTags('builds') @@ -35,7 +37,7 @@ export class BuildsController { private eventsGateway: EventsGateway, @Inject(forwardRef(() => ProjectsService)) private projectService: ProjectsService - ) { } + ) {} @Get() @ApiOkResponse({ type: PaginatedBuildDto }) @@ -59,7 +61,8 @@ export class BuildsController { @Delete(':id') @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin, Role.editor) remove(@Param('id', new ParseUUIDPipe()) id: string): Promise { return this.buildsService.remove(id); } @@ -67,7 +70,8 @@ export class BuildsController { @Post() @ApiOkResponse({ type: BuildDto }) @ApiSecurity('api_key') - @UseGuards(ApiGuard) + @UseGuards(ApiGuard, RoleGuard) + @Roles(Role.admin, Role.editor) async create(@Body() createBuildDto: CreateBuildDto): Promise { const project = await this.projectService.findOne(createBuildDto.project); await this.buildsService.deleteOldBuilds(project.id, project.maxBuildAllowed); @@ -87,7 +91,8 @@ export class BuildsController { @ApiOkResponse({ type: BuildDto }) @ApiSecurity('api_key') @ApiBearerAuth() - @UseGuards(MixedGuard) + @UseGuards(MixedGuard, RoleGuard) + @Roles(Role.admin, Role.editor) update(@Param('id', new ParseUUIDPipe()) id: string, @Body() modifyBuildDto?: ModifyBuildDto): Promise { //In future, no or empty body will do nothing as this check will be removed. It will expect a proper body to perform any patch. if (modifyBuildDto === null || Object.keys(modifyBuildDto).length === 0) { @@ -99,7 +104,8 @@ export class BuildsController { @Patch(':id/approve') @ApiOkResponse({ type: BuildDto }) @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin, Role.editor) approve( @Param('id', new ParseUUIDPipe()) id: string, @Query('merge', new ParseBoolPipe()) merge: boolean diff --git a/src/projects/projects.controller.ts b/src/projects/projects.controller.ts index 9f206c5c..70b0bf06 100644 --- a/src/projects/projects.controller.ts +++ b/src/projects/projects.controller.ts @@ -3,9 +3,11 @@ import { ApiTags, ApiBearerAuth, ApiOkResponse, ApiParam } from '@nestjs/swagger import { JwtAuthGuard } from '../auth/guards/auth.guard'; import { ProjectsService } from './projects.service'; import { CreateProjectDto } from './dto/create-project.dto'; -import { Project } from '@prisma/client'; +import { Project, Role } from '@prisma/client'; import { ProjectDto } from './dto/project.dto'; import { UpdateProjectDto } from './dto/update-project.dto'; +import { RoleGuard } from 'src/auth/guards/role.guard'; +import { Roles } from 'src/shared/roles.decorator'; @Controller('projects') @ApiTags('projects') @@ -22,7 +24,8 @@ export class ProjectsController { @Post() @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin, Role.editor) @ApiOkResponse({ type: ProjectDto }) create(@Body() createProjectDto: CreateProjectDto): Promise { return this.projectsService.create(createProjectDto); @@ -30,7 +33,8 @@ export class ProjectsController { @Put() @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin, Role.editor) @ApiOkResponse({ type: ProjectDto }) update(@Body() projectDto: UpdateProjectDto): Promise { return this.projectsService.update(projectDto); @@ -38,7 +42,8 @@ export class ProjectsController { @Delete(':id') @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin, Role.editor) @ApiOkResponse({ type: ProjectDto }) @ApiParam({ name: 'id', required: true }) remove(@Param('id', new ParseUUIDPipe()) id: string): Promise { diff --git a/src/shared/roles.decorator.ts b/src/shared/roles.decorator.ts new file mode 100644 index 00000000..d19e6754 --- /dev/null +++ b/src/shared/roles.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; +import { Role } from '@prisma/client'; + +export const Roles = (...roles: Role[]) => SetMetadata('roles', roles); diff --git a/src/test-runs/test-runs.controller.ts b/src/test-runs/test-runs.controller.ts index 217bfa04..7a7f1d3e 100644 --- a/src/test-runs/test-runs.controller.ts +++ b/src/test-runs/test-runs.controller.ts @@ -25,7 +25,7 @@ import { ApiBody, } from '@nestjs/swagger'; import { JwtAuthGuard } from '../auth/guards/auth.guard'; -import { TestRun, TestStatus, User } from '@prisma/client'; +import { Role, TestRun, TestStatus, User } from '@prisma/client'; import { TestRunsService } from './test-runs.service'; import { TestRunResultDto } from './dto/testRunResult.dto'; import { ApiGuard } from '../auth/guards/api.guard'; @@ -38,6 +38,8 @@ import { UpdateIgnoreAreasDto } from './dto/update-ignore-area.dto'; import { UpdateTestRunDto } from './dto/update-test.dto'; import { Reflector } from '@nestjs/core'; import { CurrentUser } from '../shared/current-user.decorator'; +import { RoleGuard } from 'src/auth/guards/role.guard'; +import { Roles } from 'src/shared/roles.decorator'; @ApiTags('test-runs') @Controller('test-runs') @@ -57,7 +59,8 @@ export class TestRunsController { @Post('approve') @ApiQuery({ name: 'merge', required: false }) @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin, Role.editor) async approveTestRun( @CurrentUser() user: User, @Body() ids: string[], @@ -71,7 +74,8 @@ export class TestRunsController { @Post('reject') @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin, Role.editor) async reject(@Body() ids: string[]): Promise { this.logger.debug(`Going to reject TestRuns: ${ids}`); for (const id of ids) { @@ -81,7 +85,8 @@ export class TestRunsController { @Post('delete') @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin, Role.editor) async delete(@Body() ids: string[]): Promise { this.logger.debug(`Going to delete TestRuns: ${ids}`); for (const id of ids) { @@ -91,7 +96,8 @@ export class TestRunsController { @Post('ignoreAreas/update') @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin, Role.editor) async updateIgnoreAreas(@Body() data: UpdateIgnoreAreasDto): Promise { this.logger.debug(`Going to update IgnoreAreas for TestRuns: ${data.ids}`); for (const id of data.ids) { @@ -101,7 +107,8 @@ export class TestRunsController { @Post('ignoreAreas/add') @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin, Role.editor) async addIgnoreAreas(@Body() data: UpdateIgnoreAreasDto): Promise { this.logger.debug(`Going to add IgnoreAreas for TestRuns: ${data.ids}`); for (const id of data.ids) { @@ -112,7 +119,8 @@ export class TestRunsController { @Patch('update/:testRunId') @ApiParam({ name: 'testRunId', required: true }) @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin, Role.editor) update(@Param('testRunId', new ParseUUIDPipe()) id: string, @Body() body: UpdateTestRunDto): Promise { this.logger.debug(`Going to update TestRuns: ${id}`); return this.testRunsService.update(id, body); @@ -121,7 +129,8 @@ export class TestRunsController { @Post() @ApiSecurity('api_key') @ApiOkResponse({ type: TestRunResultDto }) - @UseGuards(ApiGuard) + @UseGuards(ApiGuard, RoleGuard) + @Roles(Role.admin, Role.editor) postTestRun(@Body() createTestRequestDto: CreateTestRequestBase64Dto): Promise { const imageBuffer = Buffer.from(createTestRequestDto.imageBase64, 'base64'); return this.testRunsService.postTestRun({ @@ -135,7 +144,8 @@ export class TestRunsController { @ApiBody({ type: CreateTestRequestMultipartDto }) @ApiOkResponse({ type: TestRunResultDto }) @ApiConsumes('multipart/form-data') - @UseGuards(ApiGuard) + @UseGuards(ApiGuard, RoleGuard) + @Roles(Role.admin, Role.editor) @UseInterceptors(FileInterceptor('image'), FileToBodyInterceptor) @UsePipes(new ValidationPipe({ transform: true })) postTestRunMultipart(@Body() createTestRequestDto: CreateTestRequestMultipartDto): Promise { diff --git a/src/test-variations/test-variations.controller.ts b/src/test-variations/test-variations.controller.ts index ce13c2a1..0ba3b827 100644 --- a/src/test-variations/test-variations.controller.ts +++ b/src/test-variations/test-variations.controller.ts @@ -1,10 +1,12 @@ import { Controller, ParseUUIDPipe, Get, UseGuards, Param, Query, Delete } from '@nestjs/common'; import { ApiTags, ApiParam, ApiBearerAuth, ApiQuery, ApiOkResponse } from '@nestjs/swagger'; import { TestVariationsService } from './test-variations.service'; -import { TestVariation, Baseline } from '@prisma/client'; +import { TestVariation, Baseline, Role } from '@prisma/client'; import { JwtAuthGuard } from '../auth/guards/auth.guard'; import { PrismaService } from '../prisma/prisma.service'; import { BuildDto } from '../builds/dto/build.dto'; +import { RoleGuard } from 'src/auth/guards/role.guard'; +import { Roles } from 'src/shared/roles.decorator'; @ApiTags('test-variations') @Controller('test-variations') @@ -34,7 +36,8 @@ export class TestVariationsController { @ApiQuery({ name: 'branchName', required: true }) @ApiOkResponse({ type: BuildDto }) @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin, Role.editor) merge( @Query('projectId', new ParseUUIDPipe()) projectId: string, @Query('branchName') branchName: string @@ -45,7 +48,8 @@ export class TestVariationsController { @Delete(':id') @ApiParam({ name: 'id', required: true }) @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin, Role.editor) delete(@Param('id', new ParseUUIDPipe()) id: string): Promise { return this.testVariations.delete(id); } From a850cd2ad6a446db11ae4080e0fd0987d8f6cc49 Mon Sep 17 00:00:00 2001 From: Pavlo Strunkin Date: Sat, 24 Jul 2021 18:12:28 +0300 Subject: [PATCH 2/5] admin endpoints --- src/users/dto/user-update.dto.ts | 10 ++- src/users/dto/user.dto.ts | 6 +- src/users/users.controller.ts | 133 +++++++++++++++++++------------ src/users/users.service.ts | 4 - 4 files changed, 94 insertions(+), 59 deletions(-) diff --git a/src/users/dto/user-update.dto.ts b/src/users/dto/user-update.dto.ts index 63028500..e2b63c6c 100644 --- a/src/users/dto/user-update.dto.ts +++ b/src/users/dto/user-update.dto.ts @@ -1,8 +1,6 @@ -import { - IsString, - IsEmail, -} from 'class-validator'; +import { IsString, IsEmail, IsEnum } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { Role } from '@prisma/client'; export class UpdateUserDto { @ApiProperty() @@ -16,4 +14,8 @@ export class UpdateUserDto { @ApiProperty() @IsString() readonly lastName: string; + + @ApiProperty() + @IsEnum(Role) + readonly role: Role; } diff --git a/src/users/dto/user.dto.ts b/src/users/dto/user.dto.ts index d8584076..47afa7ef 100644 --- a/src/users/dto/user.dto.ts +++ b/src/users/dto/user.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { User } from '@prisma/client'; +import { Role, User } from '@prisma/client'; export class UserDto { @ApiProperty() @@ -17,11 +17,15 @@ export class UserDto { @ApiProperty() readonly apiKey: string; + @ApiProperty() + readonly role: Role; + constructor(user: User) { this.id = user.id; this.email = user.email; this.firstName = user.firstName; this.lastName = user.lastName; this.apiKey = user.apiKey; + this.role = user.role; } } diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index cc2ddc29..c97c3e59 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -1,62 +1,95 @@ -import { Controller, Post, Body, Get, UseGuards, Param, ParseUUIDPipe, Put } from '@nestjs/common'; +import { Controller, Post, Body, Get, UseGuards, Put, Delete, Param, ParseUUIDPipe, Patch } from '@nestjs/common'; import { UsersService } from './users.service'; -import { ApiOkResponse, ApiParam, ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ApiOkResponse, ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { UserLoginResponseDto } from './dto/user-login-response.dto'; import { CreateUserDto } from './dto/user-create.dto'; import { JwtAuthGuard } from '../auth/guards/auth.guard'; import { UserDto } from './dto/user.dto'; import { UpdateUserDto } from './dto/user-update.dto'; import { UserLoginRequestDto } from './dto/user-login-request.dto'; -import { CurrentUser } from '../shared/current-user.decorator' -import { User } from '@prisma/client'; +import { CurrentUser } from '../shared/current-user.decorator'; +import { Role, User } from '@prisma/client'; +import { RoleGuard } from 'src/auth/guards/role.guard'; +import { Roles } from 'src/shared/roles.decorator'; +import { PrismaService } from 'src/prisma/prisma.service'; @Controller('users') @ApiTags('users') export class UsersController { - constructor( - private usersService: UsersService, - ) { } - - @Post('register') - @ApiOkResponse({ type: UserLoginResponseDto }) - register( - @Body() createUserDto: CreateUserDto, - ): Promise { - return this.usersService.create(createUserDto); - } - - @Post('login') - @ApiOkResponse({ type: UserLoginResponseDto }) - async login( - @Body() userLoginRequestDto: UserLoginRequestDto, - ): Promise { - return this.usersService.login(userLoginRequestDto); - } - - @Get('newApiKey') - @ApiOkResponse({ type: String }) - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - generateNewApiKey(@CurrentUser() user: User): Promise { - return this.usersService.generateNewApiKey(user) - } - - @Put('password') - @ApiOkResponse({ type: Boolean }) - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - changePassword(@CurrentUser() user: User, @Body('password') password: string): Promise { - return this.usersService.changePassword(user, password) - } - - @Put() - @ApiOkResponse({ type: UserLoginResponseDto }) - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - update( - @CurrentUser() user: User, - @Body() updateUserDto: UpdateUserDto - ): Promise { - return this.usersService.update(user.id, updateUserDto); - } + constructor(private usersService: UsersService, private prismaService: PrismaService) {} + + @Post('register') + @ApiOkResponse({ type: UserLoginResponseDto }) + register(@Body() createUserDto: CreateUserDto): Promise { + return this.usersService.create(createUserDto); + } + + @Post('login') + @ApiOkResponse({ type: UserLoginResponseDto }) + async login(@Body() userLoginRequestDto: UserLoginRequestDto): Promise { + return this.usersService.login(userLoginRequestDto); + } + + @Get('newApiKey') + @ApiOkResponse({ type: String }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + generateNewApiKey(@CurrentUser() user: User): Promise { + return this.usersService.generateNewApiKey(user); + } + + @Put('password') + @ApiOkResponse({ type: Boolean }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + changePassword(@CurrentUser() user: User, @Body('password') password: string): Promise { + return this.usersService.changePassword(user, password); + } + + @Put() + @ApiOkResponse({ type: UserLoginResponseDto }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + update(@CurrentUser() user: User, @Body() updateUserDto: UpdateUserDto): Promise { + return this.usersService.update(user.id, updateUserDto); + } + + @Get('all') + @ApiOkResponse({ type: [UserDto] }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin) + async userList(): Promise { + const users = await this.prismaService.user.findMany(); + return users.map((user) => new UserDto(user)); + } + + @Delete(':id') + @ApiOkResponse({ type: Boolean }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin) + async delete(@Param('id', new ParseUUIDPipe()) id: string): Promise { + const user = await this.prismaService.user.delete({ where: { id } }); + return !!user; + } + + @Patch('assignRole/:id') + @ApiOkResponse({ type: UserDto }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, RoleGuard) + @Roles(Role.admin) + async assignRole( + @Param('id', new ParseUUIDPipe()) id: string, + @Body() data: Pick + ): Promise { + const user = await this.prismaService.user.update({ + where: { id }, + data: { + role: data.role, + }, + }); + + return new UserDto(user); + } } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 5e9ba9ee..3c963dae 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -12,10 +12,6 @@ import { UserLoginRequestDto } from './dto/user-login-request.dto'; export class UsersService { constructor(private prismaService: PrismaService, private authService: AuthService) {} - userList(): Promise { - return this.prismaService.user.findMany(); - } - async create(createUserDto: CreateUserDto): Promise { const user = { email: createUserDto.email.trim().toLowerCase(), From d3633ec728fd40c69d6bc1f02e9715d0db42d1a4 Mon Sep 17 00:00:00 2001 From: Pavlo Strunkin Date: Mon, 26 Jul 2021 17:51:01 +0300 Subject: [PATCH 3/5] fixed --- src/auth/guards/role.guard.ts | 3 +-- src/builds/builds.controller.ts | 4 ++-- src/projects/projects.controller.spec.ts | 6 +++++- src/projects/projects.controller.ts | 4 ++-- src/test-runs/test-runs.controller.ts | 4 ++-- src/test-variations/test-variations.controller.ts | 4 ++-- src/users/users.controller.spec.ts | 6 +++++- src/users/users.controller.ts | 6 +++--- 8 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/auth/guards/role.guard.ts b/src/auth/guards/role.guard.ts index 62a04d08..2db119f1 100644 --- a/src/auth/guards/role.guard.ts +++ b/src/auth/guards/role.guard.ts @@ -1,4 +1,4 @@ -import { ExecutionContext, Injectable, CanActivate, UnauthorizedException } from '@nestjs/common'; +import { ExecutionContext, Injectable, CanActivate } from '@nestjs/common'; import { Role, User } from '@prisma/client'; import { Request } from 'express'; import { PrismaService } from '../../prisma/prisma.service'; @@ -19,7 +19,6 @@ export class RoleGuard implements CanActivate { } checkPermission = (user: User): boolean => { - console.log(user); switch (user.role) { case Role.admin: { return true; diff --git a/src/builds/builds.controller.ts b/src/builds/builds.controller.ts index b3fa9e5a..05daf125 100644 --- a/src/builds/builds.controller.ts +++ b/src/builds/builds.controller.ts @@ -26,8 +26,8 @@ import { PaginatedBuildDto } from './dto/build-paginated.dto'; import { ModifyBuildDto } from './dto/build-modify.dto'; import { ProjectsService } from '../projects/projects.service'; import { EventsGateway } from '../shared/events/events.gateway'; -import { RoleGuard } from 'src/auth/guards/role.guard'; -import { Roles } from 'src/shared/roles.decorator'; +import { RoleGuard } from '../auth/guards/role.guard'; +import { Roles } from '../shared/roles.decorator'; @Controller('builds') @ApiTags('builds') diff --git a/src/projects/projects.controller.spec.ts b/src/projects/projects.controller.spec.ts index ce52c690..b61f2783 100644 --- a/src/projects/projects.controller.spec.ts +++ b/src/projects/projects.controller.spec.ts @@ -1,4 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../prisma/prisma.service'; import { ProjectsController } from './projects.controller'; import { ProjectsService } from './projects.service'; @@ -8,7 +9,10 @@ describe('Projects Controller', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ProjectsController], - providers: [{ provide: ProjectsService, useValue: {} }] + providers: [ + { provide: ProjectsService, useValue: {} }, + { provide: PrismaService, useValue: {} }, + ], }).compile(); controller = module.get(ProjectsController); diff --git a/src/projects/projects.controller.ts b/src/projects/projects.controller.ts index 70b0bf06..c255397e 100644 --- a/src/projects/projects.controller.ts +++ b/src/projects/projects.controller.ts @@ -6,8 +6,8 @@ import { CreateProjectDto } from './dto/create-project.dto'; import { Project, Role } from '@prisma/client'; import { ProjectDto } from './dto/project.dto'; import { UpdateProjectDto } from './dto/update-project.dto'; -import { RoleGuard } from 'src/auth/guards/role.guard'; -import { Roles } from 'src/shared/roles.decorator'; +import { RoleGuard } from '../auth/guards/role.guard'; +import { Roles } from '../shared/roles.decorator'; @Controller('projects') @ApiTags('projects') diff --git a/src/test-runs/test-runs.controller.ts b/src/test-runs/test-runs.controller.ts index 7a7f1d3e..e4b15c34 100644 --- a/src/test-runs/test-runs.controller.ts +++ b/src/test-runs/test-runs.controller.ts @@ -38,8 +38,8 @@ import { UpdateIgnoreAreasDto } from './dto/update-ignore-area.dto'; import { UpdateTestRunDto } from './dto/update-test.dto'; import { Reflector } from '@nestjs/core'; import { CurrentUser } from '../shared/current-user.decorator'; -import { RoleGuard } from 'src/auth/guards/role.guard'; -import { Roles } from 'src/shared/roles.decorator'; +import { RoleGuard } from '../auth/guards/role.guard'; +import { Roles } from '../shared/roles.decorator'; @ApiTags('test-runs') @Controller('test-runs') diff --git a/src/test-variations/test-variations.controller.ts b/src/test-variations/test-variations.controller.ts index 0ba3b827..6a4fe7bc 100644 --- a/src/test-variations/test-variations.controller.ts +++ b/src/test-variations/test-variations.controller.ts @@ -5,8 +5,8 @@ import { TestVariation, Baseline, Role } from '@prisma/client'; import { JwtAuthGuard } from '../auth/guards/auth.guard'; import { PrismaService } from '../prisma/prisma.service'; import { BuildDto } from '../builds/dto/build.dto'; -import { RoleGuard } from 'src/auth/guards/role.guard'; -import { Roles } from 'src/shared/roles.decorator'; +import { RoleGuard } from '../auth/guards/role.guard'; +import { Roles } from '../shared/roles.decorator'; @ApiTags('test-variations') @Controller('test-variations') diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts index dc032175..f52b4474 100644 --- a/src/users/users.controller.spec.ts +++ b/src/users/users.controller.spec.ts @@ -1,4 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../prisma/prisma.service'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; @@ -8,7 +9,10 @@ describe('Users Controller', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [UsersController], - providers: [{ provide: UsersService, useValue: {} }] + providers: [ + { provide: UsersService, useValue: {} }, + { provide: PrismaService, useValue: {} }, + ], }).compile(); controller = module.get(UsersController); diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index c97c3e59..5e44e812 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -9,9 +9,9 @@ import { UpdateUserDto } from './dto/user-update.dto'; import { UserLoginRequestDto } from './dto/user-login-request.dto'; import { CurrentUser } from '../shared/current-user.decorator'; import { Role, User } from '@prisma/client'; -import { RoleGuard } from 'src/auth/guards/role.guard'; -import { Roles } from 'src/shared/roles.decorator'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { RoleGuard } from '../auth/guards/role.guard'; +import { Roles } from '../shared/roles.decorator'; +import { PrismaService } from '../prisma/prisma.service'; @Controller('users') @ApiTags('users') From 0ece1e4be20cb2183bdcd4af7acab793033347d7 Mon Sep 17 00:00:00 2001 From: Pavlo Strunkin Date: Thu, 29 Jul 2021 11:34:25 +0300 Subject: [PATCH 4/5] updated --- src/users/dto/assign-role.dto.ts | 13 +++++++++++++ src/users/dto/user-update.dto.ts | 7 +------ src/users/users.controller.ts | 19 +++++-------------- src/users/users.service.ts | 10 ++++++++++ test/preconditions.ts | 7 ++++--- 5 files changed, 33 insertions(+), 23 deletions(-) create mode 100644 src/users/dto/assign-role.dto.ts diff --git a/src/users/dto/assign-role.dto.ts b/src/users/dto/assign-role.dto.ts new file mode 100644 index 00000000..780040e1 --- /dev/null +++ b/src/users/dto/assign-role.dto.ts @@ -0,0 +1,13 @@ +import { IsEnum, IsUUID } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { Role } from '@prisma/client'; + +export class AssignRoleDto { + @ApiProperty() + @IsUUID() + readonly id: string; + + @ApiProperty() + @IsEnum(Role) + readonly role: Role; +} diff --git a/src/users/dto/user-update.dto.ts b/src/users/dto/user-update.dto.ts index e2b63c6c..fdb3c556 100644 --- a/src/users/dto/user-update.dto.ts +++ b/src/users/dto/user-update.dto.ts @@ -1,6 +1,5 @@ -import { IsString, IsEmail, IsEnum } from 'class-validator'; +import { IsString, IsEmail } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; -import { Role } from '@prisma/client'; export class UpdateUserDto { @ApiProperty() @@ -14,8 +13,4 @@ export class UpdateUserDto { @ApiProperty() @IsString() readonly lastName: string; - - @ApiProperty() - @IsEnum(Role) - readonly role: Role; } diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 5e44e812..64a4a65d 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -12,6 +12,7 @@ import { Role, User } from '@prisma/client'; import { RoleGuard } from '../auth/guards/role.guard'; import { Roles } from '../shared/roles.decorator'; import { PrismaService } from '../prisma/prisma.service'; +import { AssignRoleDto } from './dto/assign-role.dto'; @Controller('users') @ApiTags('users') @@ -70,26 +71,16 @@ export class UsersController { @UseGuards(JwtAuthGuard, RoleGuard) @Roles(Role.admin) async delete(@Param('id', new ParseUUIDPipe()) id: string): Promise { - const user = await this.prismaService.user.delete({ where: { id } }); + const user = await this.usersService.delete(id); return !!user; } - @Patch('assignRole/:id') + @Patch('assignRole') @ApiOkResponse({ type: UserDto }) @ApiBearerAuth() @UseGuards(JwtAuthGuard, RoleGuard) @Roles(Role.admin) - async assignRole( - @Param('id', new ParseUUIDPipe()) id: string, - @Body() data: Pick - ): Promise { - const user = await this.prismaService.user.update({ - where: { id }, - data: { - role: data.role, - }, - }); - - return new UserDto(user); + async assignRole(@Body() data: AssignRoleDto): Promise { + return this.usersService.assignRole(data); } } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 3c963dae..7be3d833 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -7,6 +7,7 @@ import { UserDto } from './dto/user.dto'; import { UpdateUserDto } from './dto/user-update.dto'; import { AuthService } from '../auth/auth.service'; import { UserLoginRequestDto } from './dto/user-login-request.dto'; +import { AssignRoleDto } from './dto/assign-role.dto'; @Injectable() export class UsersService { @@ -41,6 +42,15 @@ export class UsersService { return new UserDto(user); } + async assignRole(data: AssignRoleDto): Promise { + const { id, role } = data; + const user = await this.prismaService.user.update({ + where: { id }, + data: { role }, + }); + return new UserDto(user); + } + async update(id: string, userDto: UpdateUserDto): Promise { const user = await this.prismaService.user.update({ where: { id }, diff --git a/test/preconditions.ts b/test/preconditions.ts index cdb5629d..5bfc9f41 100644 --- a/test/preconditions.ts +++ b/test/preconditions.ts @@ -7,10 +7,9 @@ import { TestRunsService } from 'src/test-runs/test-runs.service'; import { readFileSync } from 'fs'; import { TestRunResultDto } from 'src/test-runs/dto/testRunResult.dto'; import { Build } from '@prisma/client'; +import { CreateUserDto } from 'src/users/dto/user-create.dto'; -export const generateUser = ( - password: string -): { email: string; password: string; firstName: string; lastName: string } => ({ +export const generateUser = (password: string): CreateUserDto => ({ email: `${uuidAPIKey.create().uuid}@example.com'`, password, firstName: 'fName', @@ -38,6 +37,8 @@ export const haveUserLogged = async (usersService: UsersService) => { const password = '123456'; const user = await usersService.create(generateUser(password)); + await usersService.assignRole({ id: user.id, role: 'admin' }); + return usersService.login({ email: user.email, password, From f744f1eb9519f48139de71a97bba32d577531ccc Mon Sep 17 00:00:00 2001 From: Pavlo Strunkin Date: Thu, 29 Jul 2021 12:52:13 +0300 Subject: [PATCH 5/5] refactor --- src/test-runs/test-runs.controller.ts | 3 +-- src/users/users.controller.ts | 14 ++++++++++---- src/users/users.service.ts | 6 ++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/test-runs/test-runs.controller.ts b/src/test-runs/test-runs.controller.ts index e4b15c34..84f8ac50 100644 --- a/src/test-runs/test-runs.controller.ts +++ b/src/test-runs/test-runs.controller.ts @@ -36,7 +36,6 @@ import { CreateTestRequestMultipartDto } from './dto/create-test-request-multipa import { FileToBodyInterceptor } from '../shared/fite-to-body.interceptor'; import { UpdateIgnoreAreasDto } from './dto/update-ignore-area.dto'; import { UpdateTestRunDto } from './dto/update-test.dto'; -import { Reflector } from '@nestjs/core'; import { CurrentUser } from '../shared/current-user.decorator'; import { RoleGuard } from '../auth/guards/role.guard'; import { Roles } from '../shared/roles.decorator'; @@ -46,7 +45,7 @@ import { Roles } from '../shared/roles.decorator'; export class TestRunsController { private readonly logger: Logger = new Logger(TestRunsController.name); - constructor(private testRunsService: TestRunsService, private reflector: Reflector) {} + constructor(private testRunsService: TestRunsService) {} @Get() @ApiOkResponse({ type: [TestRunDto] }) diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 64a4a65d..65d3e854 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -13,10 +13,13 @@ import { RoleGuard } from '../auth/guards/role.guard'; import { Roles } from '../shared/roles.decorator'; import { PrismaService } from '../prisma/prisma.service'; import { AssignRoleDto } from './dto/assign-role.dto'; +import { Logger } from '@nestjs/common'; @Controller('users') @ApiTags('users') export class UsersController { + private readonly logger: Logger = new Logger(UsersController.name); + constructor(private usersService: UsersService, private prismaService: PrismaService) {} @Post('register') @@ -65,14 +68,16 @@ export class UsersController { return users.map((user) => new UserDto(user)); } - @Delete(':id') + @Delete() @ApiOkResponse({ type: Boolean }) @ApiBearerAuth() @UseGuards(JwtAuthGuard, RoleGuard) @Roles(Role.admin) - async delete(@Param('id', new ParseUUIDPipe()) id: string): Promise { - const user = await this.usersService.delete(id); - return !!user; + async delete(@Body() ids: string[]): Promise { + this.logger.debug(`Going to remove User: ${ids}`); + for (const id of ids) { + await this.usersService.delete(id); + } } @Patch('assignRole') @@ -81,6 +86,7 @@ export class UsersController { @UseGuards(JwtAuthGuard, RoleGuard) @Roles(Role.admin) async assignRole(@Body() data: AssignRoleDto): Promise { + this.logger.debug(`Going to assign role ${data.role} to User: ${data.id}`); return this.usersService.assignRole(data); } } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 7be3d833..70da8b73 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -8,9 +8,12 @@ import { UpdateUserDto } from './dto/user-update.dto'; import { AuthService } from '../auth/auth.service'; import { UserLoginRequestDto } from './dto/user-login-request.dto'; import { AssignRoleDto } from './dto/assign-role.dto'; +import { Logger } from '@nestjs/common'; @Injectable() export class UsersService { + private readonly logger: Logger = new Logger(UsersService.name); + constructor(private prismaService: PrismaService, private authService: AuthService) {} async create(createUserDto: CreateUserDto): Promise { @@ -34,6 +37,7 @@ export class UsersService { } async delete(id: string): Promise { + this.logger.debug(`Removing User: ${id}`); return this.prismaService.user.delete({ where: { id } }); } @@ -44,6 +48,8 @@ export class UsersService { async assignRole(data: AssignRoleDto): Promise { const { id, role } = data; + this.logger.debug(`Assigning role ${role} to User: ${id}`); + const user = await this.prismaService.user.update({ where: { id }, data: { role },