diff --git a/.depcheckrc.json b/.depcheckrc.json index f6dd2749f..eb9aa31c3 100644 --- a/.depcheckrc.json +++ b/.depcheckrc.json @@ -9,7 +9,6 @@ "@commitlint/cli", "@types/node", "stylelint", - "@prisma/client", "is-docker" ] } diff --git a/.npmrc b/.npmrc index 3970e9dc4..869fc9547 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,5 @@ save-exact=true stream=true -strict-peer-dependencies=false \ No newline at end of file +strict-peer-dependencies=false +public-hoist-pattern[]=prisma +public-hoist-pattern[]=@prisma/client \ No newline at end of file diff --git a/apps/backend/.eslintrc.cjs b/apps/backend/.eslintrc.cjs index 87993d430..54d463993 100644 --- a/apps/backend/.eslintrc.cjs +++ b/apps/backend/.eslintrc.cjs @@ -14,4 +14,12 @@ module.exports = { 'node/no-sync': 'error', }, + overrides: [ + { + files: ['./src/data/**/*.ts'], + rules: { + 'max-lines': 'off', + }, + }, + ], }; diff --git a/apps/backend/src/data/depcheck-data.ts b/apps/backend/src/data/depcheck-data.ts new file mode 100644 index 000000000..3457cdb7a --- /dev/null +++ b/apps/backend/src/data/depcheck-data.ts @@ -0,0 +1,10 @@ +import type { ILibraryData } from '../interfaces/libraries-data'; + +export const depcheckData: ILibraryData = { + name: 'Depcheck', + author: 'Djordje Lukic, Junle Li', + description: 'Check your npm module for unused dependencies.', + types: ['Linters'], + categories: ['Dependencies'], + language: 'JavaScript', +}; diff --git a/apps/frontend/src/data/eslint-data.ts b/apps/backend/src/data/eslint-data.ts similarity index 99% rename from apps/frontend/src/data/eslint-data.ts rename to apps/backend/src/data/eslint-data.ts index 900f945ce..861ea7074 100644 --- a/apps/frontend/src/data/eslint-data.ts +++ b/apps/backend/src/data/eslint-data.ts @@ -1,12 +1,12 @@ -import { LibraryCategory } from '../models/library-category'; -import { LibraryType } from '../models/library-type'; +import type { ILibraryData } from '../interfaces/libraries-data'; -export const eslintData = { +export const eslintData: ILibraryData = { name: 'ESLint', author: 'Nicholas C. Zakas', description: 'Find and fix problems in your JavaScript code.', - type: [LibraryType.Linters], - category: [LibraryCategory.Code], + types: ['Linters'], + categories: ['Code'], + language: 'JavaScript', rules: { 'Array Callback Return': { description: 'Enforce `return` statements in callbacks of array methods', diff --git a/apps/backend/src/data/inflint-data.ts b/apps/backend/src/data/inflint-data.ts new file mode 100644 index 000000000..5ee3b65cb --- /dev/null +++ b/apps/backend/src/data/inflint-data.ts @@ -0,0 +1,10 @@ +import type { ILibraryData } from '../interfaces/libraries-data'; + +export const inflintData: ILibraryData = { + name: 'Inflint', + author: 'Tal Rofe', + description: 'Inflint is a tool which scans and verifies file name conventions.', + types: ['Linters'], + categories: ['File System'], + language: 'Agnostic', +}; diff --git a/apps/frontend/src/data/libraries-data.ts b/apps/backend/src/data/libraries-data.ts similarity index 50% rename from apps/frontend/src/data/libraries-data.ts rename to apps/backend/src/data/libraries-data.ts index 4e9e5fd49..0085f601f 100644 --- a/apps/frontend/src/data/libraries-data.ts +++ b/apps/backend/src/data/libraries-data.ts @@ -1,14 +1,14 @@ -import type { ILbirariesData } from '../interfaces/libraries'; +import type { ILibraryData } from '../interfaces/libraries-data'; import { depcheckData } from './depcheck-data'; import { eslintData } from './eslint-data'; import { inflintData } from './inflint-data'; import { prettierData } from './prettier-data'; import { stylelintData } from './stylelint-data'; -export const librariesData: ILbirariesData = { - eslint: eslintData, - stylelint: stylelintData, - depcheck: depcheckData, - prettier: prettierData, - inflint: inflintData, -}; +export const librariesData: ILibraryData[] = [ + depcheckData, + eslintData, + inflintData, + prettierData, + stylelintData, +]; diff --git a/apps/frontend/src/data/prettier-data.ts b/apps/backend/src/data/prettier-data.ts similarity index 95% rename from apps/frontend/src/data/prettier-data.ts rename to apps/backend/src/data/prettier-data.ts index ccacc75ba..e49a80fa6 100644 --- a/apps/frontend/src/data/prettier-data.ts +++ b/apps/backend/src/data/prettier-data.ts @@ -1,12 +1,11 @@ -import { LibraryCategory } from '../models/library-category'; -import { LibraryType } from '../models/library-type'; +import type { ILibraryData } from '../interfaces/libraries-data'; -export const prettierData = { +export const prettierData: ILibraryData = { name: 'Prettier', author: 'prettier.io', description: 'Prettier is an opinionated code formatter.', - type: [LibraryType.Formatters], - category: [LibraryCategory.Code], + types: ['Formatters'], + categories: ['Code'], rules: { 'Print Width': { description: 'Specify the line length that the printer will wrap on.', @@ -100,4 +99,5 @@ export const prettierData = { configApi: 'singleAttributePerLine', }, }, + language: 'Agnostic', }; diff --git a/apps/frontend/src/data/stylelint-data.ts b/apps/backend/src/data/stylelint-data.ts similarity index 99% rename from apps/frontend/src/data/stylelint-data.ts rename to apps/backend/src/data/stylelint-data.ts index ef5dcbc0c..29568f9cc 100644 --- a/apps/frontend/src/data/stylelint-data.ts +++ b/apps/backend/src/data/stylelint-data.ts @@ -1,13 +1,13 @@ -import { LibraryCategory } from '../models/library-category'; -import { LibraryType } from '../models/library-type'; +import type { ILibraryData } from '../interfaces/libraries-data'; -export const stylelintData = { +export const stylelintData: ILibraryData = { name: 'Stylelint', author: 'stylelint.io', description: 'A mighty, modern linter that helps you avoid errors and enforce conventions in your styles.', - type: [LibraryType.Linters], - category: [LibraryCategory.Code, LibraryCategory.Styles], + types: ['Linters'], + categories: ['Code', 'Styles'], + language: 'CSSHTML', rules: { 'Color No Invalid Hex': { description: 'Disallow invalid hex colors.', diff --git a/apps/backend/src/interfaces/libraries-data.ts b/apps/backend/src/interfaces/libraries-data.ts new file mode 100644 index 000000000..eea149066 --- /dev/null +++ b/apps/backend/src/interfaces/libraries-data.ts @@ -0,0 +1,24 @@ +import type { PolicyLibrary } from '@prisma/client'; + +interface ILibraryRule { + readonly description: string; + readonly configApi: string; + readonly hasAutoFix?: boolean; + readonly category?: string; +} + +export type IType = 'Linters' | 'Formatters'; + +export type ICategory = 'Code' | 'File System' | 'Styles' | 'Dependencies'; + +export type ILanguage = 'JavaScript' | 'CSSHTML' | 'Agnostic'; + +export interface ILibraryData { + readonly name: PolicyLibrary; + readonly author: string; + readonly description: string; + readonly types: IType[]; + readonly categories: ICategory[]; + readonly language: ILanguage; + readonly rules?: Record; +} diff --git a/apps/backend/src/modules/database/inline-policy.service.ts b/apps/backend/src/modules/database/inline-policy.service.ts index 21983d1ae..191b66778 100644 --- a/apps/backend/src/modules/database/inline-policy.service.ts +++ b/apps/backend/src/modules/database/inline-policy.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import type { PolicyLibrary, Prisma } from '@prisma/client'; +import type { PolicyLibrary } from '@prisma/client'; import { PrismaService } from './prisma.service'; @@ -7,23 +7,14 @@ import { PrismaService } from './prisma.service'; export class DBInlinePolicyService { constructor(private prisma: PrismaService) {} - public async createInlinePolicy(groupId: string, label: string, library: PolicyLibrary) { - const createdInlinePolicy = await this.prisma.inlinePolicy.create({ - data: { groupId, label, library }, - select: { id: true }, - }); - - return createdInlinePolicy.id; - } - - public async deleteInlinePolicy(inlinePolicyId: string) { - await this.prisma.inlinePolicy.delete({ where: { id: inlinePolicyId } }); - } - - public async updateConfiguration(inlinePolicyId: string, configuration: Record) { - await this.prisma.inlinePolicy.update({ - where: { id: inlinePolicyId }, - data: { configuration: configuration as Prisma.JsonObject }, + public async createInlinePolicy( + groupId: string, + label: string, + description: string | null, + library: PolicyLibrary, + ) { + await this.prisma.inlinePolicy.create({ + data: { groupId, label, description, library }, }); } @@ -35,54 +26,21 @@ export class DBInlinePolicyService { return inlinePolicyDB !== null; } - public async addRule(inlinePolicyId: string, rule: Record) { - const inlinePolicyDB = await this.prisma.inlinePolicy.findUniqueOrThrow({ - where: { id: inlinePolicyId }, - select: { rules: true }, + public async isLabelAvailable(userId: string, label: string) { + const record = await this.prisma.inlinePolicy.findFirst({ + where: { label, group: { userId } }, }); - let newInlinePolicyRules: Prisma.JsonObject; - - if (!inlinePolicyDB.rules) { - newInlinePolicyRules = rule as Prisma.JsonObject; - } else { - newInlinePolicyRules = { - ...(inlinePolicyDB.rules as Prisma.JsonObject), - ...rule, - } as Prisma.JsonObject; - } - - await this.prisma.inlinePolicy.update({ - where: { id: inlinePolicyId }, - data: { rules: newInlinePolicyRules }, - }); + return record === null; } - public async removeRule(inlinePolicyId: string, ruleName: string) { - const inlinePolicyDB = await this.prisma.inlinePolicy.findUniqueOrThrow({ - where: { id: inlinePolicyId }, - select: { rules: true }, - }); - - if (!inlinePolicyDB.rules) { - return; - } - - const rulesWithoutRule = { - ...(inlinePolicyDB.rules as Prisma.JsonObject), - [ruleName]: undefined, - }; - - await this.prisma.inlinePolicy.update({ - where: { id: inlinePolicyId }, - data: { rules: rulesWithoutRule }, - }); + public getUserGroupLibraries(groupId: string) { + return this.prisma.inlinePolicy.findMany({ where: { groupId }, select: { library: true } }); } - public getConfiguration(inlinePolicyId: string) { - return this.prisma.inlinePolicy.findFirst({ - where: { id: inlinePolicyId }, - select: { configuration: true }, - }); + public async groupHasLibrary(groupId: string, library: PolicyLibrary) { + const record = await this.prisma.inlinePolicy.findFirst({ where: { groupId, library } }); + + return record !== null; } } diff --git a/apps/backend/src/modules/user/modules/groups/classes/responses.ts b/apps/backend/src/modules/user/modules/groups/classes/responses.ts index adab62191..c1a729870 100644 --- a/apps/backend/src/modules/user/modules/groups/classes/responses.ts +++ b/apps/backend/src/modules/user/modules/groups/classes/responses.ts @@ -1,8 +1,9 @@ import { ApiResponseProperty } from '@nestjs/swagger'; import { PolicyLibrary } from '@prisma/client'; -import type { IGroupInlinePolicies, IGroupInlinePolicy } from '../interfaces/group-policies'; +import { ILanguage } from '@/interfaces/libraries-data'; +import type { IGroupInlinePolicies, IGroupInlinePolicy } from '../interfaces/group-policies'; import type { IUserGroupGetAll, IUserGroupInlinePolicy } from '../interfaces/user-group'; class UserGroupInlinePolicyGetAll implements IUserGroupInlinePolicy { @@ -71,7 +72,7 @@ class GroupInlinePolicy implements IGroupInlinePolicy { type: String, example: 'JavaScript', }) - public language!: string; + public language!: ILanguage; } export class CreateGroupResponse { diff --git a/apps/backend/src/modules/user/modules/groups/data/libraries.ts b/apps/backend/src/modules/user/modules/groups/data/libraries.ts deleted file mode 100644 index 1e256c70f..000000000 --- a/apps/backend/src/modules/user/modules/groups/data/libraries.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { PolicyLibrary } from '@prisma/client'; - -export const libariesLanguages: Record = { - ESLint: 'JavaScript', - Stylelint: 'CSS', - Depcheck: 'JavaScript', - Inflint: 'Agnostic', - Prettier: 'Agnostic', -}; diff --git a/apps/backend/src/modules/user/modules/groups/interfaces/group-policies.ts b/apps/backend/src/modules/user/modules/groups/interfaces/group-policies.ts index 141734b08..c7717b599 100644 --- a/apps/backend/src/modules/user/modules/groups/interfaces/group-policies.ts +++ b/apps/backend/src/modules/user/modules/groups/interfaces/group-policies.ts @@ -1,7 +1,9 @@ import type { Group, InlinePolicy } from '@prisma/client'; +import type { ILanguage } from '@/interfaces/libraries-data'; + export type IGroupInlinePolicy = Pick & { - readonly language: string; + readonly language: ILanguage; }; export interface IGroupInlinePolicies extends Pick { diff --git a/apps/backend/src/modules/user/modules/groups/queries/handlers/get-inline-policies.handler.ts b/apps/backend/src/modules/user/modules/groups/queries/handlers/get-inline-policies.handler.ts index 7e622a838..dc7886190 100644 --- a/apps/backend/src/modules/user/modules/groups/queries/handlers/get-inline-policies.handler.ts +++ b/apps/backend/src/modules/user/modules/groups/queries/handlers/get-inline-policies.handler.ts @@ -1,9 +1,9 @@ import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; import { DBGroupService } from '@/modules/database/group.service'; +import { librariesData } from '@/data/libraries-data'; import { GetInlinePoliciesContract } from '../contracts/get-inline-policies.contract'; -import { libariesLanguages } from '../../data/libraries'; @QueryHandler(GetInlinePoliciesContract) export class GetInlinePoliciesHandler implements IQueryHandler { @@ -17,10 +17,16 @@ export class GetInlinePoliciesHandler implements IQueryHandler ({ - ...inlinePolicy, - language: libariesLanguages[inlinePolicy.library], - })); + data.inlinePolicies = data.inlinePolicies.map((inlinePolicy) => { + const matchingLibraryData = librariesData.find( + (libraryData) => libraryData.name === inlinePolicy.library, + )!; + + return { + ...inlinePolicy, + language: matchingLibraryData.language, + }; + }); return data; } diff --git a/apps/backend/src/modules/user/modules/inline-policies/add-rule.controller.ts b/apps/backend/src/modules/user/modules/inline-policies/add-rule.controller.ts deleted file mode 100644 index b22f0740f..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/add-rule.controller.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { - Body, - Controller, - HttpCode, - HttpStatus, - Logger, - Param, - Patch, - Post, - UseGuards, -} from '@nestjs/common'; -import { CommandBus } from '@nestjs/cqrs'; -import { RealIP } from 'nestjs-real-ip'; -import { - ApiBearerAuth, - ApiCreatedResponse, - ApiInternalServerErrorResponse, - ApiOkResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; - -import { CurrentUserId } from '@/decorators/current-user-id.decorator'; - -import Routes from './inline-policies.routes'; -import { BelongingInlinePolicyGuard } from './guards/belonging-inline-policy.guard'; -import { AddRuleDto } from './classes/add-rule.dto'; -import { AddRuleContract } from './commands/contracts/add-rule.contract'; -import { EditRuleContract } from './commands/contracts/edit-rule.contract'; - -@ApiTags('Inline Policies') -@Controller(Routes.CONTROLLER) -export class AddRuleController { - private readonly logger = new Logger(AddRuleController.name); - - constructor(private readonly commandBus: CommandBus) {} - - @ApiOperation({ description: 'Add a new rule for an inline policy' }) - @ApiBearerAuth('access-token') - @ApiCreatedResponse({ description: 'If successfully added the rule' }) - @ApiUnauthorizedResponse({ - description: 'If access token is missing or invalid, or the policy does not belong to user', - }) - @ApiInternalServerErrorResponse({ description: 'If failed to add rule' }) - @UseGuards(BelongingInlinePolicyGuard) - @Post(Routes.ADD_RULE) - @HttpCode(HttpStatus.CREATED) - public async addRule( - @CurrentUserId() userId: string, - @Param('policy_id') policyId: string, - @Body() addRuleDto: AddRuleDto, - @RealIP() ip: string, - ): Promise { - this.logger.log(`Will try to add rule for an inline policy with an Id: "${policyId}"`); - - await this.commandBus.execute( - new AddRuleContract(policyId, addRuleDto.rule, userId, ip), - ); - - this.logger.log(`Successfully added a rule for an inline policy Id: "${policyId}"`); - } - - @ApiOperation({ description: 'Edit a rule of an inline policy' }) - @ApiBearerAuth('access-token') - @ApiOkResponse({ description: 'If successfully edited the rule' }) - @ApiUnauthorizedResponse({ - description: 'If access token is missing or invalid, or the policy does not belong to user', - }) - @ApiInternalServerErrorResponse({ description: 'If failed to edit rule' }) - @UseGuards(BelongingInlinePolicyGuard) - @Patch(Routes.EDIT_RULE) - @HttpCode(HttpStatus.OK) - public async editRule( - @Param('policy_id') policyId: string, - @Body() editRuleDto: AddRuleDto, - ): Promise { - this.logger.log(`Will try to edit rule for an inline policy with an Id: "${policyId}"`); - - await this.commandBus.execute( - new EditRuleContract(policyId, editRuleDto.rule), - ); - - this.logger.log(`Successfully edited a rule for an inline policy Id: "${policyId}"`); - } -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/available-label.controller.ts b/apps/backend/src/modules/user/modules/inline-policies/available-label.controller.ts new file mode 100644 index 000000000..dddba72dc --- /dev/null +++ b/apps/backend/src/modules/user/modules/inline-policies/available-label.controller.ts @@ -0,0 +1,57 @@ +import { Controller, Get, HttpCode, HttpStatus, Logger, Param } from '@nestjs/common'; +import { QueryBus } from '@nestjs/cqrs'; +import { + ApiBearerAuth, + ApiInternalServerErrorResponse, + ApiOkResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; + +import { CurrentUserId } from '@/decorators/current-user-id.decorator'; + +import Routes from './inline-policies.routes'; +import { AvailableLabelResponse } from './classes/responses'; +import { AvailableLabelContract } from './queries/contracts/available-label.contract'; + +@ApiTags('Inline Policies') +@Controller(Routes.CONTROLLER) +export class AvailableLabelController { + private readonly logger = new Logger(AvailableLabelController.name); + + constructor(private readonly queryBus: QueryBus) {} + + @ApiOperation({ description: 'Check whether a provided label is availble' }) + @ApiBearerAuth('access-token') + @ApiOkResponse({ + description: 'Returns whether the provided label is available', + type: AvailableLabelResponse, + }) + @ApiUnauthorizedResponse({ + description: 'If access token is invalid or missing', + }) + @ApiInternalServerErrorResponse({ description: 'If failed to get availability status of the label' }) + @Get(Routes.AVAILABLE_LABEL) + @HttpCode(HttpStatus.OK) + public async availableLabel( + @CurrentUserId() userId: string, + @Param('label') label: string, + ): Promise { + this.logger.log( + `Will try to get availability status of label: "${label}" with a user ID: "${userId}"`, + ); + + const isAvailable = await this.queryBus.execute( + new AvailableLabelContract(userId, label), + ); + + this.logger.log( + `Successfully got availability status of label: "${label}" with a user ID: "${userId}"`, + ); + + return { + isAvailable, + }; + } +} diff --git a/apps/backend/src/modules/user/modules/inline-policies/classes/add-rule.dto.ts b/apps/backend/src/modules/user/modules/inline-policies/classes/add-rule.dto.ts deleted file mode 100644 index bdb327bbe..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/classes/add-rule.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsJSON } from 'class-validator'; - -export class AddRuleDto { - @ApiProperty({ - type: String, - description: 'Stringified rules object', - example: JSON.stringify({ yazifRule: 'error' }), - }) - @IsJSON() - readonly rule!: string; -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/classes/create-inline.dto.ts b/apps/backend/src/modules/user/modules/inline-policies/classes/create.dto.ts similarity index 51% rename from apps/backend/src/modules/user/modules/inline-policies/classes/create-inline.dto.ts rename to apps/backend/src/modules/user/modules/inline-policies/classes/create.dto.ts index 4aae7359d..2d36380d5 100644 --- a/apps/backend/src/modules/user/modules/inline-policies/classes/create-inline.dto.ts +++ b/apps/backend/src/modules/user/modules/inline-policies/classes/create.dto.ts @@ -1,13 +1,25 @@ import { ApiProperty } from '@nestjs/swagger'; import { PolicyLibrary } from '@prisma/client'; -import { IsEnum, IsString, MinLength } from 'class-validator'; +import { IsEnum, IsString, MaxLength, MinLength } from 'class-validator'; -export class CreateInlineDto { +import { IsNullable } from '@/decorators/is-nullable.decorator'; + +export class CreateDto { @ApiProperty({ type: String, description: 'The label of the new inline policy', example: 'Yazif Policy' }) @IsString() + @MaxLength(30) @MinLength(1) readonly label!: string; + @ApiProperty({ + type: String, + description: 'The description of the new inline policy', + example: 'Yazif Policy description', + }) + @IsString() + @IsNullable() + readonly description!: string | null; + @ApiProperty({ enum: PolicyLibrary, description: 'The library being used by the policy' }) @IsEnum(PolicyLibrary) readonly library!: PolicyLibrary; diff --git a/apps/backend/src/modules/user/modules/inline-policies/classes/remove-rule.dto.ts b/apps/backend/src/modules/user/modules/inline-policies/classes/remove-rule.dto.ts deleted file mode 100644 index 85364050c..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/classes/remove-rule.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString, MinLength } from 'class-validator'; - -export class RemoveRuleDto { - @ApiProperty({ type: String, description: 'The name of the rule to remove', example: 'YazifRule' }) - @IsString() - @MinLength(1) - readonly ruleName!: string; -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/classes/responses.ts b/apps/backend/src/modules/user/modules/inline-policies/classes/responses.ts index 07260bec3..72bcb843c 100644 --- a/apps/backend/src/modules/user/modules/inline-policies/classes/responses.ts +++ b/apps/backend/src/modules/user/modules/inline-policies/classes/responses.ts @@ -1,18 +1,56 @@ import { ApiResponseProperty } from '@nestjs/swagger'; -import { Prisma } from '@prisma/client'; +import { PolicyLibrary } from '@prisma/client'; + +import { type ICategory, ILanguage, type ILibraryData, type IType } from '@/interfaces/libraries-data'; + +class GetLibrary implements Omit { + @ApiResponseProperty({ + enum: PolicyLibrary, + }) + public name!: PolicyLibrary; + + @ApiResponseProperty({ + type: String, + example: 'Yazif', + }) + public author!: string; + + @ApiResponseProperty({ + type: String, + example: 'Nice library by Yazif', + }) + public description!: string; + + @ApiResponseProperty({ + type: [String], + example: ['Linters'], + }) + public types!: IType[]; + + @ApiResponseProperty({ + type: [String], + example: ['Code'], + }) + public categories!: ICategory[]; -export class CreateInlinePolicyResponse { @ApiResponseProperty({ type: String, - example: '62e5362119bea07115434f4a', + example: 'JavaScript', + }) + public language!: ILanguage; +} + +export class AvailableLabelResponse { + @ApiResponseProperty({ + type: Boolean, + example: true, }) - public policyId!: string; + public isAvailable!: boolean; } -export class GetConfigurationResponse { +export class GetLibrariesResponse { @ApiResponseProperty({ - type: Object, - example: { yazifConfig1: 'Yazif', yazifConfig2: 'Yazif 2' }, + type: [GetLibrary], }) - public configuration!: Prisma.JsonValue; + public libraries!: Omit[]; } diff --git a/apps/backend/src/modules/user/modules/inline-policies/classes/update-configuration.dto.ts b/apps/backend/src/modules/user/modules/inline-policies/classes/update-configuration.dto.ts deleted file mode 100644 index 895aa92ae..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/classes/update-configuration.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsJSON } from 'class-validator'; - -export class UpdateConfigurationDto { - @ApiProperty({ - type: String, - description: 'Stringified configuration object', - example: JSON.stringify({ yazifConfig1: 'yazif', yazifConfig2: 'yazifos' }), - }) - @IsJSON() - readonly configuration!: string; -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/add-rule.contract.ts b/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/add-rule.contract.ts deleted file mode 100644 index 466564133..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/add-rule.contract.ts +++ /dev/null @@ -1,8 +0,0 @@ -export class AddRuleContract { - constructor( - public readonly policyId: string, - public readonly rule: string, - public readonly userId: string, - public readonly ip: string, - ) {} -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/queries/contracts/create-inline.contract.ts b/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/create.contract.ts similarity index 76% rename from apps/backend/src/modules/user/modules/inline-policies/queries/contracts/create-inline.contract.ts rename to apps/backend/src/modules/user/modules/inline-policies/commands/contracts/create.contract.ts index 97168bdfe..8e86701e4 100644 --- a/apps/backend/src/modules/user/modules/inline-policies/queries/contracts/create-inline.contract.ts +++ b/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/create.contract.ts @@ -1,11 +1,12 @@ import type { PolicyLibrary } from '@prisma/client'; -export class CreateInlineContract { +export class CreateContract { constructor( + public readonly userId: string, public readonly groupId: string, public readonly label: string, + public readonly description: string | null, public readonly library: PolicyLibrary, - public readonly userId: string, public readonly ip: string, ) {} } diff --git a/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/delete-inline.contract.ts b/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/delete-inline.contract.ts deleted file mode 100644 index 6df58e988..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/delete-inline.contract.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class DeleteInlineContract { - constructor(public readonly policyId: string) {} -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/edit-rule.contract.ts b/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/edit-rule.contract.ts deleted file mode 100644 index 8c06bf927..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/edit-rule.contract.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class EditRuleContract { - constructor(public readonly policyId: string, public readonly rule: string) {} -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/remove-rule.contract.ts b/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/remove-rule.contract.ts deleted file mode 100644 index b9fa31747..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/remove-rule.contract.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class RemoveRuleContract { - constructor(public readonly policyId: string, public readonly ruleName: string) {} -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/update-configuration.contract.ts b/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/update-configuration.contract.ts deleted file mode 100644 index 98b67a4e3..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/commands/contracts/update-configuration.contract.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class UpdateConfigurationContract { - constructor(public readonly policyId: string, public readonly configuration: string) {} -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/add-rule.handler.ts b/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/add-rule.handler.ts deleted file mode 100644 index c366625f9..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/add-rule.handler.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CommandHandler, EventBus, type ICommandHandler } from '@nestjs/cqrs'; - -import { DBInlinePolicyService } from '@/modules/database/inline-policy.service'; - -import { AddRuleContract } from '../contracts/add-rule.contract'; -import { CreateRuleMixpanelContract } from '../../events/contracts/create-rule-mixpanel.contract'; - -@CommandHandler(AddRuleContract) -export class AddRuleHandler implements ICommandHandler { - constructor( - private readonly dbInlinePolicyService: DBInlinePolicyService, - private readonly eventBus: EventBus, - ) {} - - async execute(contract: AddRuleContract) { - const parsedRule = JSON.parse(contract.rule); - - await this.dbInlinePolicyService.addRule(contract.policyId, parsedRule); - - this.eventBus.publish(new CreateRuleMixpanelContract(contract.userId, contract.ip)); - } -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/create.handler.ts b/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/create.handler.ts new file mode 100644 index 000000000..64aef60d0 --- /dev/null +++ b/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/create.handler.ts @@ -0,0 +1,25 @@ +import { CommandHandler, EventBus, type ICommandHandler } from '@nestjs/cqrs'; + +import { DBInlinePolicyService } from '@/modules/database/inline-policy.service'; + +import { CreateMixpanelContract } from '../../events/contracts/create-mixpanel.contract'; +import { CreateContract } from '../contracts/create.contract'; + +@CommandHandler(CreateContract) +export class CreateInlineHandler implements ICommandHandler { + constructor( + private readonly dbInlinePolicyService: DBInlinePolicyService, + private readonly eventBus: EventBus, + ) {} + + async execute(contract: CreateContract) { + await this.dbInlinePolicyService.createInlinePolicy( + contract.groupId, + contract.label, + contract.description, + contract.library, + ); + + this.eventBus.publish(new CreateMixpanelContract(contract.userId, contract.ip)); + } +} diff --git a/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/delete-inline.handler.ts b/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/delete-inline.handler.ts deleted file mode 100644 index 5f810280c..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/delete-inline.handler.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs'; - -import { DBInlinePolicyService } from '@/modules/database/inline-policy.service'; - -import { DeleteInlineContract } from '../contracts/delete-inline.contract'; - -@CommandHandler(DeleteInlineContract) -export class DeleteInlineHandler implements ICommandHandler { - constructor(private readonly dbInlinePolicyService: DBInlinePolicyService) {} - - async execute(contract: DeleteInlineContract) { - await this.dbInlinePolicyService.deleteInlinePolicy(contract.policyId); - } -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/edit-rule.handler.ts b/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/edit-rule.handler.ts deleted file mode 100644 index c0e401075..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/edit-rule.handler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs'; - -import { DBInlinePolicyService } from '@/modules/database/inline-policy.service'; - -import { EditRuleContract } from '../contracts/edit-rule.contract'; - -@CommandHandler(EditRuleContract) -export class EditRuleHandler implements ICommandHandler { - constructor(private readonly dbInlinePolicyService: DBInlinePolicyService) {} - - async execute(contract: EditRuleContract) { - const parsedRule = JSON.parse(contract.rule); - - await this.dbInlinePolicyService.addRule(contract.policyId, parsedRule); - } -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/index.ts b/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/index.ts index e0318af2f..e5e23ff6c 100644 --- a/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/index.ts +++ b/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/index.ts @@ -1,13 +1,3 @@ -import { AddRuleHandler } from './add-rule.handler'; -import { DeleteInlineHandler } from './delete-inline.handler'; -import { EditRuleHandler } from './edit-rule.handler'; -import { RemoveRuleHandler } from './remove-rule.handler'; -import { UpdateConfigurationHandler } from './update-configuration.handler'; +import { CreateInlineHandler } from './create.handler'; -export const CommandHandlers = [ - DeleteInlineHandler, - UpdateConfigurationHandler, - AddRuleHandler, - RemoveRuleHandler, - EditRuleHandler, -]; +export const CommandHandlers = [CreateInlineHandler]; diff --git a/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/remove-rule.handler.ts b/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/remove-rule.handler.ts deleted file mode 100644 index d3059ba5a..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/remove-rule.handler.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs'; - -import { DBInlinePolicyService } from '@/modules/database/inline-policy.service'; - -import { RemoveRuleContract } from '../contracts/remove-rule.contract'; - -@CommandHandler(RemoveRuleContract) -export class RemoveRuleHandler implements ICommandHandler { - constructor(private readonly dbInlinePolicyService: DBInlinePolicyService) {} - - async execute(contract: RemoveRuleContract) { - await this.dbInlinePolicyService.removeRule(contract.policyId, contract.ruleName); - } -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/update-configuration.handler.ts b/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/update-configuration.handler.ts deleted file mode 100644 index 7bf93b8dd..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/commands/handlers/update-configuration.handler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs'; - -import { DBInlinePolicyService } from '@/modules/database/inline-policy.service'; - -import { UpdateConfigurationContract } from '../contracts/update-configuration.contract'; - -@CommandHandler(UpdateConfigurationContract) -export class UpdateConfigurationHandler implements ICommandHandler { - constructor(private readonly dbInlinePolicyService: DBInlinePolicyService) {} - - async execute(contract: UpdateConfigurationContract) { - const parsedConfiguration = JSON.parse(contract.configuration); - - await this.dbInlinePolicyService.updateConfiguration(contract.policyId, parsedConfiguration); - } -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/create-inline.controller.ts b/apps/backend/src/modules/user/modules/inline-policies/create-inline.controller.ts deleted file mode 100644 index 83e1b72b7..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/create-inline.controller.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Body, Controller, HttpCode, HttpStatus, Logger, Param, Post, UseGuards } from '@nestjs/common'; -import { QueryBus } from '@nestjs/cqrs'; -import { RealIP } from 'nestjs-real-ip'; -import { - ApiBearerAuth, - ApiCreatedResponse, - ApiInternalServerErrorResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; - -import { CurrentUserId } from '@/decorators/current-user-id.decorator'; -import { BelongingGroupGuard } from '@/guards/belonging-group.guard'; - -import Routes from './inline-policies.routes'; -import { CreateInlineDto } from './classes/create-inline.dto'; -import { CreateInlineContract } from './queries/contracts/create-inline.contract'; -import { CreateInlinePolicyResponse } from './classes/responses'; - -@ApiTags('Inline Policies') -@Controller(Routes.CONTROLLER) -export class CreateInlineController { - private readonly logger = new Logger(CreateInlineController.name); - - constructor(private readonly queryBus: QueryBus) {} - - @ApiOperation({ description: 'Create a new inline policy with label and chosen library' }) - @ApiBearerAuth('access-token') - @ApiCreatedResponse({ - description: 'If successfully created the policy', - type: CreateInlinePolicyResponse, - }) - @ApiUnauthorizedResponse({ - description: 'If access token is missing or invalid', - }) - @ApiInternalServerErrorResponse({ description: 'If failed to create policy' }) - @UseGuards(BelongingGroupGuard) - @Post(Routes.CREATE) - @HttpCode(HttpStatus.CREATED) - public async createInline( - @CurrentUserId() userId: string, - @Param('group_id') groupId: string, - @Body() createInlineDto: CreateInlineDto, - @RealIP() ip: string, - ): Promise { - this.logger.log( - `Will try to create an inline policy for a user with an Id: "${userId}" and for group with Id: "${groupId}". Label is "${createInlineDto.label}"`, - ); - - const createdInlinePolicyId = await this.queryBus.execute( - new CreateInlineContract(groupId, createInlineDto.label, createInlineDto.library, userId, ip), - ); - - this.logger.log( - `Successfully created an inline policy for a user with an Id: "${userId}" and for group with Id: "${groupId}". Label is "${createInlineDto.label}"`, - ); - - return { - policyId: createdInlinePolicyId, - }; - } -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/create.controller.ts b/apps/backend/src/modules/user/modules/inline-policies/create.controller.ts new file mode 100644 index 000000000..09d6940fd --- /dev/null +++ b/apps/backend/src/modules/user/modules/inline-policies/create.controller.ts @@ -0,0 +1,83 @@ +import { + BadRequestException, + Body, + Controller, + HttpCode, + HttpStatus, + Logger, + Param, + Post, + UseGuards, +} from '@nestjs/common'; +import { CommandBus, QueryBus } from '@nestjs/cqrs'; +import { RealIP } from 'nestjs-real-ip'; +import { + ApiBearerAuth, + ApiCreatedResponse, + ApiInternalServerErrorResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; + +import { CurrentUserId } from '@/decorators/current-user-id.decorator'; +import { BelongingGroupGuard } from '@/guards/belonging-group.guard'; + +import Routes from './inline-policies.routes'; +import { CreateDto } from './classes/create.dto'; +import { CreateContract } from './commands/contracts/create.contract'; +import { GroupHasLibraryContract } from './queries/contracts/group-has-library.contract'; + +@ApiTags('Inline Policies') +@Controller(Routes.CONTROLLER) +export class createController { + private readonly logger = new Logger(createController.name); + + constructor(private readonly commandBus: CommandBus, private readonly queryBus: QueryBus) {} + + @ApiOperation({ description: 'Create a new inline policy with label, description and chosen library' }) + @ApiBearerAuth('access-token') + @ApiCreatedResponse({ + description: 'If successfully created the inline policy', + }) + @ApiUnauthorizedResponse({ + description: 'If access token is missing or invalid', + }) + @ApiInternalServerErrorResponse({ description: 'If failed to create inline policy' }) + @UseGuards(BelongingGroupGuard) + @Post(Routes.CREATE) + @HttpCode(HttpStatus.CREATED) + public async create( + @CurrentUserId() userId: string, + @Param('group_id') groupId: string, + @Body() createDto: CreateDto, + @RealIP() ip: string, + ): Promise { + this.logger.log( + `Will try to create an inline policy for a user with an Id: "${userId}" and for group with Id: "${groupId}". Label is "${createDto.label}"`, + ); + + const groupHasLibrary = await this.queryBus.execute( + new GroupHasLibraryContract(groupId, createDto.library), + ); + + if (groupHasLibrary) { + throw new BadRequestException(); + } + + await this.commandBus.execute( + new CreateContract( + userId, + groupId, + createDto.label, + createDto.description, + createDto.library, + ip, + ), + ); + + this.logger.log( + `Successfully created an inline policy for a user with an Id: "${userId}" and for group with Id: "${groupId}". Label is "${createDto.label}"`, + ); + } +} diff --git a/apps/backend/src/modules/user/modules/inline-policies/delete-inline.controller.ts b/apps/backend/src/modules/user/modules/inline-policies/delete-inline.controller.ts deleted file mode 100644 index 6126d879a..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/delete-inline.controller.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Controller, Delete, HttpCode, HttpStatus, Logger, Param, UseGuards } from '@nestjs/common'; -import { CommandBus } from '@nestjs/cqrs'; -import { - ApiBearerAuth, - ApiInternalServerErrorResponse, - ApiOkResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; - -import { CurrentUserId } from '@/decorators/current-user-id.decorator'; - -import Routes from './inline-policies.routes'; -import { DeleteInlineContract } from './commands/contracts/delete-inline.contract'; -import { BelongingInlinePolicyGuard } from './guards/belonging-inline-policy.guard'; - -@ApiTags('Inline Policies') -@Controller(Routes.CONTROLLER) -export class DeleteInlineController { - private readonly logger = new Logger(DeleteInlineController.name); - - constructor(private readonly commandBus: CommandBus) {} - - @ApiOperation({ description: 'Delete a policy by its identifier' }) - @ApiBearerAuth('access-token') - @ApiOkResponse({ description: 'If successfully deleted the policy' }) - @ApiUnauthorizedResponse({ - description: 'If access token is missing or invalid, or policy does not belong to user', - }) - @ApiInternalServerErrorResponse({ description: 'If failed to delete policy' }) - @UseGuards(BelongingInlinePolicyGuard) - @Delete(Routes.DELETE) - @HttpCode(HttpStatus.OK) - public async deleteInline( - @CurrentUserId() userId: string, - @Param('policy_id') policyId: string, - ): Promise { - this.logger.log( - `Will try to delete an inline policy for a user with an Id: "${userId}" and an inline policy Id: "${policyId}"`, - ); - - await this.commandBus.execute(new DeleteInlineContract(policyId)); - - this.logger.log( - `Successfully deleted an inline policy for a user with an Id: "${userId}" and an inline policy Id: "${policyId}"`, - ); - } -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/events/contracts/create-rule-mixpanel.contract.ts b/apps/backend/src/modules/user/modules/inline-policies/events/contracts/create-mixpanel.contract.ts similarity index 65% rename from apps/backend/src/modules/user/modules/inline-policies/events/contracts/create-rule-mixpanel.contract.ts rename to apps/backend/src/modules/user/modules/inline-policies/events/contracts/create-mixpanel.contract.ts index 794cffd02..fc83a6aae 100644 --- a/apps/backend/src/modules/user/modules/inline-policies/events/contracts/create-rule-mixpanel.contract.ts +++ b/apps/backend/src/modules/user/modules/inline-policies/events/contracts/create-mixpanel.contract.ts @@ -1,3 +1,3 @@ -export class CreateRuleMixpanelContract { +export class CreateMixpanelContract { constructor(public readonly userId: string, public readonly ip: string) {} } diff --git a/apps/backend/src/modules/user/modules/inline-policies/events/contracts/create-policy-mixpanel.contract.ts b/apps/backend/src/modules/user/modules/inline-policies/events/contracts/create-policy-mixpanel.contract.ts deleted file mode 100644 index 1767f69f8..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/events/contracts/create-policy-mixpanel.contract.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class CreatePolicyMixpanelContract { - constructor(public readonly userId: string, public readonly ip: string) {} -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/events/handlers/create-policy-mixpanel.handler.ts b/apps/backend/src/modules/user/modules/inline-policies/events/handlers/create-mixpanel.handler.ts similarity index 66% rename from apps/backend/src/modules/user/modules/inline-policies/events/handlers/create-policy-mixpanel.handler.ts rename to apps/backend/src/modules/user/modules/inline-policies/events/handlers/create-mixpanel.handler.ts index d7e6d0f78..97d3674e8 100644 --- a/apps/backend/src/modules/user/modules/inline-policies/events/handlers/create-policy-mixpanel.handler.ts +++ b/apps/backend/src/modules/user/modules/inline-policies/events/handlers/create-mixpanel.handler.ts @@ -4,14 +4,14 @@ import Mixpanel from 'mixpanel'; import type { IEnvironment } from '@/config/env.interface'; -import { CreatePolicyMixpanelContract } from '../contracts/create-policy-mixpanel.contract'; +import { CreateMixpanelContract } from '../contracts/create-mixpanel.contract'; import { INLINE_POLICY_CREATE } from '../../models/mixpanel-events'; -@CommandHandler(CreatePolicyMixpanelContract) -export class CreatePolicyMixpanelHandler implements ICommandHandler { +@CommandHandler(CreateMixpanelContract) +export class CreateMixpanelHandler implements ICommandHandler { constructor(private readonly configService: ConfigService) {} - execute(contract: CreatePolicyMixpanelContract): Promise { + execute(contract: CreateMixpanelContract): Promise { const mixpanel = Mixpanel.init(this.configService.get('mixpanelToken', { infer: true })); mixpanel.track(INLINE_POLICY_CREATE, { diff --git a/apps/backend/src/modules/user/modules/inline-policies/events/handlers/create-rule-mixpanel.handler.ts b/apps/backend/src/modules/user/modules/inline-policies/events/handlers/create-rule-mixpanel.handler.ts deleted file mode 100644 index f6d88b079..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/events/handlers/create-rule-mixpanel.handler.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ConfigService } from '@nestjs/config'; -import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs'; -import Mixpanel from 'mixpanel'; - -import type { IEnvironment } from '@/config/env.interface'; - -import { CreateRuleMixpanelContract } from '../contracts/create-rule-mixpanel.contract'; -import { RULE_CREATE } from '../../models/mixpanel-events'; - -@CommandHandler(CreateRuleMixpanelContract) -export class CreateRuleMixpanelHandler implements ICommandHandler { - constructor(private readonly configService: ConfigService) {} - - execute(contract: CreateRuleMixpanelContract): Promise { - const mixpanel = Mixpanel.init(this.configService.get('mixpanelToken', { infer: true })); - - mixpanel.track(RULE_CREATE, { - distinct_id: contract.userId, - ip: contract.ip, - }); - - return Promise.resolve(); - } -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/events/handlers/index.ts b/apps/backend/src/modules/user/modules/inline-policies/events/handlers/index.ts index ab1976e2d..ef6875920 100644 --- a/apps/backend/src/modules/user/modules/inline-policies/events/handlers/index.ts +++ b/apps/backend/src/modules/user/modules/inline-policies/events/handlers/index.ts @@ -1,4 +1,3 @@ -import { CreatePolicyMixpanelHandler } from './create-policy-mixpanel.handler'; -import { CreateRuleMixpanelHandler } from './create-rule-mixpanel.handler'; +import { CreateMixpanelHandler } from './create-mixpanel.handler'; -export const EventHandlers = [CreatePolicyMixpanelHandler, CreateRuleMixpanelHandler]; +export const EventHandlers = [CreateMixpanelHandler]; diff --git a/apps/backend/src/modules/user/modules/inline-policies/get-configuration.controller.ts b/apps/backend/src/modules/user/modules/inline-policies/get-configuration.controller.ts deleted file mode 100644 index a298fdba6..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/get-configuration.controller.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Controller, Get, HttpCode, HttpStatus, Logger, Param, UseGuards } from '@nestjs/common'; -import { QueryBus } from '@nestjs/cqrs'; -import type { Prisma } from '@prisma/client'; -import { - ApiBearerAuth, - ApiInternalServerErrorResponse, - ApiOkResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; - -import { CurrentUserId } from '@/decorators/current-user-id.decorator'; - -import Routes from './inline-policies.routes'; -import { BelongingInlinePolicyGuard } from './guards/belonging-inline-policy.guard'; -import { GetConfigurationResponse } from './classes/responses'; -import { GetConfigurationContract } from './queries/contracts/get-configuration.contract'; - -@ApiTags('Inline Policies') -@Controller(Routes.CONTROLLER) -export class GetConfigurationController { - private readonly logger = new Logger(GetConfigurationController.name); - - constructor(private readonly queryBus: QueryBus) {} - - @ApiOperation({ description: 'Get the configuration of a policy by its identifer' }) - @ApiBearerAuth('access-token') - @ApiOkResponse({ - description: 'If successfully fetched the policy configuration', - type: GetConfigurationResponse, - }) - @ApiUnauthorizedResponse({ - description: 'If access token is missing or invalid, or policy does not belong to user', - }) - @ApiInternalServerErrorResponse({ description: 'If failed to fetch policy configuration' }) - @UseGuards(BelongingInlinePolicyGuard) - @Get(Routes.GET_CONFIGURATION) - @HttpCode(HttpStatus.OK) - public async getConfiguration( - @CurrentUserId() userId: string, - @Param('policy_id') policyId: string, - ): Promise { - this.logger.log( - `Will try to fetch policy configuration belongs to user with an Id: "${userId}" with policy Id: "${policyId}"`, - ); - - const policyConfiguration = await this.queryBus.execute( - new GetConfigurationContract(policyId), - ); - - this.logger.log( - `Successfully got policy configuration belongs to user with an Id: "${userId}" with policy Id: "${policyId}"`, - ); - - return { - configuration: policyConfiguration, - }; - } -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/get-libraries.controller.ts b/apps/backend/src/modules/user/modules/inline-policies/get-libraries.controller.ts new file mode 100644 index 000000000..e40b05c4c --- /dev/null +++ b/apps/backend/src/modules/user/modules/inline-policies/get-libraries.controller.ts @@ -0,0 +1,62 @@ +import { Controller, Get, HttpCode, HttpStatus, Logger, Param, UseGuards } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiOkResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; +import { QueryBus } from '@nestjs/cqrs'; +import type { PolicyLibrary } from '@prisma/client'; + +import { CurrentUserId } from '@/decorators/current-user-id.decorator'; +import { librariesData } from '@/data/libraries-data'; +import { BelongingGroupGuard } from '@/guards/belonging-group.guard'; + +import Routes from './inline-policies.routes'; +import { GetLibrariesResponse } from './classes/responses'; +import { UserGroupLibrariesContract } from './queries/contracts/user-group-libraries.contract'; + +@ApiTags('Inline Policies') +@Controller(Routes.CONTROLLER) +export class GetLibrariesController { + private readonly logger = new Logger(GetLibrariesController.name); + + constructor(private readonly queryBus: QueryBus) {} + + @ApiOperation({ description: 'Fetch all libraries Exlint offers' }) + @ApiBearerAuth('access-token') + @ApiOkResponse({ + description: 'Returns libraries Exlint offers', + type: GetLibrariesResponse, + }) + @ApiUnauthorizedResponse({ + description: 'If access token is invalid or missing', + }) + @UseGuards(BelongingGroupGuard) + @Get(Routes.GET_LIBRARIES) + @HttpCode(HttpStatus.OK) + public async getLibraries( + @CurrentUserId() userId: string, + @Param('group_id') groupId: string, + ): Promise { + this.logger.log(`Will try to get libraries with a user ID: "${userId}"`); + + const groupLibraries = await this.queryBus.execute( + new UserGroupLibrariesContract(groupId), + ); + + const libraries = librariesData + .filter((library) => !groupLibraries.includes(library.name)) + .map((library) => ({ + ...library, + rules: undefined, + })); + + this.logger.log(`Successfully got libraries with a user ID: "${userId}"`); + + return { + libraries, + }; + } +} diff --git a/apps/backend/src/modules/user/modules/inline-policies/guards/belonging-inline-policy.guard.ts b/apps/backend/src/modules/user/modules/inline-policies/guards/belonging-inline-policy.guard.ts deleted file mode 100644 index 7f9e51d5b..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/guards/belonging-inline-policy.guard.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Injectable, type CanActivate, type ExecutionContext } from '@nestjs/common'; - -import type { IJwtTokenPayload } from '@/interfaces/jwt-token'; -import { DBInlinePolicyService } from '@/modules/database/inline-policy.service'; - -@Injectable() -export class BelongingInlinePolicyGuard implements CanActivate { - constructor(private readonly dbInlinePolicyService: DBInlinePolicyService) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const user = request.user as IJwtTokenPayload; - const userId = user.sub; - const inlinePolicyId = request.params.policy_id as string; - - const groupBelongUser = await this.dbInlinePolicyService.doesInlinePolicyBelongUser( - userId, - inlinePolicyId, - ); - - return groupBelongUser; - } -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/inline-policies.module.ts b/apps/backend/src/modules/user/modules/inline-policies/inline-policies.module.ts index e8063068a..0c8d7ad50 100644 --- a/apps/backend/src/modules/user/modules/inline-policies/inline-policies.module.ts +++ b/apps/backend/src/modules/user/modules/inline-policies/inline-policies.module.ts @@ -3,33 +3,16 @@ import { CqrsModule } from '@nestjs/cqrs'; import { BelongingGroupGuard } from '@/guards/belonging-group.guard'; -import { CommandHandlers } from './commands/handlers'; import { QueryHandlers } from './queries/handlers'; -import { CreateInlineController } from './create-inline.controller'; -import { DeleteInlineController } from './delete-inline.controller'; -import { BelongingInlinePolicyGuard } from './guards/belonging-inline-policy.guard'; -import { UpdateConfigurationController } from './update-configuration.controller'; -import { AddRuleController } from './add-rule.controller'; -import { RemoveRuleController } from './remove-rule.controller'; +import { CommandHandlers } from './commands/handlers'; import { EventHandlers } from './events/handlers'; -import { GetConfigurationController } from './get-configuration.controller'; +import { AvailableLabelController } from './available-label.controller'; +import { GetLibrariesController } from './get-libraries.controller'; +import { createController } from './create.controller'; @Module({ imports: [CqrsModule], - controllers: [ - CreateInlineController, - DeleteInlineController, - UpdateConfigurationController, - AddRuleController, - RemoveRuleController, - GetConfigurationController, - ], - providers: [ - BelongingGroupGuard, - BelongingInlinePolicyGuard, - ...CommandHandlers, - ...QueryHandlers, - ...EventHandlers, - ], + controllers: [AvailableLabelController, GetLibrariesController, createController], + providers: [BelongingGroupGuard, ...QueryHandlers, ...CommandHandlers, ...EventHandlers], }) export class InlinePoliciesModule {} diff --git a/apps/backend/src/modules/user/modules/inline-policies/inline-policies.routes.ts b/apps/backend/src/modules/user/modules/inline-policies/inline-policies.routes.ts index a5e10b287..7b869c82f 100644 --- a/apps/backend/src/modules/user/modules/inline-policies/inline-policies.routes.ts +++ b/apps/backend/src/modules/user/modules/inline-policies/inline-policies.routes.ts @@ -1,12 +1,8 @@ const Routes = { CONTROLLER: 'inline-policies', CREATE: ':group_id', - DELETE: ':policy_id', - UPDATE_CONFIGURATION: ':policy_id', - ADD_RULE: 'add-rule/:policy_id', - EDIT_RULE: 'edit-rule/:policy_id', - REMOVE_RULE: 'remove-rule/:policy_id', - GET_CONFIGURATION: ':policy_id', + AVAILABLE_LABEL: 'available/:label', + GET_LIBRARIES: 'libraries/:group_id', }; export default Routes; diff --git a/apps/backend/src/modules/user/modules/inline-policies/models/mixpanel-events.ts b/apps/backend/src/modules/user/modules/inline-policies/models/mixpanel-events.ts index 70698a8b1..d9b021140 100644 --- a/apps/backend/src/modules/user/modules/inline-policies/models/mixpanel-events.ts +++ b/apps/backend/src/modules/user/modules/inline-policies/models/mixpanel-events.ts @@ -1,3 +1 @@ export const INLINE_POLICY_CREATE = 'Inline_Policy_Create'; - -export const RULE_CREATE = 'Rule_Create'; diff --git a/apps/backend/src/modules/user/modules/inline-policies/queries/contracts/available-label.contract.ts b/apps/backend/src/modules/user/modules/inline-policies/queries/contracts/available-label.contract.ts new file mode 100644 index 000000000..25998701a --- /dev/null +++ b/apps/backend/src/modules/user/modules/inline-policies/queries/contracts/available-label.contract.ts @@ -0,0 +1,3 @@ +export class AvailableLabelContract { + constructor(public readonly userId: string, public readonly label: string) {} +} diff --git a/apps/backend/src/modules/user/modules/inline-policies/queries/contracts/get-configuration.contract.ts b/apps/backend/src/modules/user/modules/inline-policies/queries/contracts/get-configuration.contract.ts deleted file mode 100644 index 2c7ba9856..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/queries/contracts/get-configuration.contract.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class GetConfigurationContract { - constructor(public readonly policyId: string) {} -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/queries/contracts/group-has-library.contract.ts b/apps/backend/src/modules/user/modules/inline-policies/queries/contracts/group-has-library.contract.ts new file mode 100644 index 000000000..421b1fef4 --- /dev/null +++ b/apps/backend/src/modules/user/modules/inline-policies/queries/contracts/group-has-library.contract.ts @@ -0,0 +1,5 @@ +import type { PolicyLibrary } from '@prisma/client'; + +export class GroupHasLibraryContract { + constructor(readonly groupId: string, readonly library: PolicyLibrary) {} +} diff --git a/apps/backend/src/modules/user/modules/inline-policies/queries/contracts/user-group-libraries.contract.ts b/apps/backend/src/modules/user/modules/inline-policies/queries/contracts/user-group-libraries.contract.ts new file mode 100644 index 000000000..e8ed6d274 --- /dev/null +++ b/apps/backend/src/modules/user/modules/inline-policies/queries/contracts/user-group-libraries.contract.ts @@ -0,0 +1,3 @@ +export class UserGroupLibrariesContract { + constructor(readonly groupId: string) {} +} diff --git a/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/available-label.handler.ts b/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/available-label.handler.ts new file mode 100644 index 000000000..9b5bcf99b --- /dev/null +++ b/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/available-label.handler.ts @@ -0,0 +1,14 @@ +import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; + +import { DBInlinePolicyService } from '@/modules/database/inline-policy.service'; + +import { AvailableLabelContract } from '../contracts/available-label.contract'; + +@QueryHandler(AvailableLabelContract) +export class AvailableLabelHandler implements IQueryHandler { + constructor(private readonly dbInlinePolicyService: DBInlinePolicyService) {} + + execute(contract: AvailableLabelContract) { + return this.dbInlinePolicyService.isLabelAvailable(contract.userId, contract.label); + } +} diff --git a/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/create-inline.handler.ts b/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/create-inline.handler.ts deleted file mode 100644 index 8387da905..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/create-inline.handler.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CommandHandler, EventBus, type ICommandHandler } from '@nestjs/cqrs'; - -import { DBInlinePolicyService } from '@/modules/database/inline-policy.service'; - -import { CreateInlineContract } from '../contracts/create-inline.contract'; -import { CreatePolicyMixpanelContract } from '../../events/contracts/create-policy-mixpanel.contract'; - -@CommandHandler(CreateInlineContract) -export class CreateInlineHandler implements ICommandHandler { - constructor( - private readonly dbInlinePolicyService: DBInlinePolicyService, - private readonly eventBus: EventBus, - ) {} - - async execute(contract: CreateInlineContract) { - const createdInlinePolicyId = await this.dbInlinePolicyService.createInlinePolicy( - contract.groupId, - contract.label, - contract.library, - ); - - this.eventBus.publish(new CreatePolicyMixpanelContract(contract.userId, contract.ip)); - - return createdInlinePolicyId; - } -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/get-configuration.handler.ts b/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/get-configuration.handler.ts deleted file mode 100644 index d9f398624..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/get-configuration.handler.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs'; - -import { DBInlinePolicyService } from '@/modules/database/inline-policy.service'; - -import { GetConfigurationContract } from '../contracts/get-configuration.contract'; - -@CommandHandler(GetConfigurationContract) -export class GetConfigurationHandler implements ICommandHandler { - constructor(private readonly dbInlinePolicyService: DBInlinePolicyService) {} - - execute(contract: GetConfigurationContract) { - return this.dbInlinePolicyService.getConfiguration(contract.policyId); - } -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/group-has-library.handler.ts b/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/group-has-library.handler.ts new file mode 100644 index 000000000..0a5630f2f --- /dev/null +++ b/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/group-has-library.handler.ts @@ -0,0 +1,14 @@ +import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; + +import { DBInlinePolicyService } from '@/modules/database/inline-policy.service'; + +import { GroupHasLibraryContract } from '../contracts/group-has-library.contract'; + +@QueryHandler(GroupHasLibraryContract) +export class GroupHasLibraryHandler implements IQueryHandler { + constructor(private readonly dbInlinePolicyService: DBInlinePolicyService) {} + + execute(contract: GroupHasLibraryContract) { + return this.dbInlinePolicyService.groupHasLibrary(contract.groupId, contract.library); + } +} diff --git a/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/index.ts b/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/index.ts index a928f628a..d1dac2dda 100644 --- a/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/index.ts +++ b/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/index.ts @@ -1,4 +1,5 @@ -import { CreateInlineHandler } from './create-inline.handler'; -import { GetConfigurationHandler } from './get-configuration.handler'; +import { AvailableLabelHandler } from './available-label.handler'; +import { GroupHasLibraryHandler } from './group-has-library.handler'; +import { UserGroupLibrariesHandler } from './user-group-libraries.handler'; -export const QueryHandlers = [CreateInlineHandler, GetConfigurationHandler]; +export const QueryHandlers = [AvailableLabelHandler, UserGroupLibrariesHandler, GroupHasLibraryHandler]; diff --git a/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/user-group-libraries.handler.ts b/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/user-group-libraries.handler.ts new file mode 100644 index 000000000..ee7f01c80 --- /dev/null +++ b/apps/backend/src/modules/user/modules/inline-policies/queries/handlers/user-group-libraries.handler.ts @@ -0,0 +1,16 @@ +import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; + +import { DBInlinePolicyService } from '@/modules/database/inline-policy.service'; + +import { UserGroupLibrariesContract } from '../contracts/user-group-libraries.contract'; + +@QueryHandler(UserGroupLibrariesContract) +export class UserGroupLibrariesHandler implements IQueryHandler { + constructor(private readonly dbInlinePolicyService: DBInlinePolicyService) {} + + async execute(contract: UserGroupLibrariesContract) { + const records = await this.dbInlinePolicyService.getUserGroupLibraries(contract.groupId); + + return records.map((record) => record.library); + } +} diff --git a/apps/backend/src/modules/user/modules/inline-policies/remove-rule.controller.ts b/apps/backend/src/modules/user/modules/inline-policies/remove-rule.controller.ts deleted file mode 100644 index 1ab30b617..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/remove-rule.controller.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Body, Controller, Delete, HttpCode, HttpStatus, Logger, Param, UseGuards } from '@nestjs/common'; -import { CommandBus } from '@nestjs/cqrs'; -import { - ApiBearerAuth, - ApiInternalServerErrorResponse, - ApiOkResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; - -import Routes from './inline-policies.routes'; -import { BelongingInlinePolicyGuard } from './guards/belonging-inline-policy.guard'; -import { RemoveRuleDto } from './classes/remove-rule.dto'; -import { RemoveRuleContract } from './commands/contracts/remove-rule.contract'; - -@ApiTags('Inline Policies') -@Controller(Routes.CONTROLLER) -export class RemoveRuleController { - private readonly logger = new Logger(RemoveRuleController.name); - - constructor(private readonly commandBus: CommandBus) {} - - @ApiOperation({ description: 'Remove a rule (by its name) of a policiy by its identifier' }) - @ApiBearerAuth('access-token') - @ApiOkResponse({ description: 'If successfully deleted the rule' }) - @ApiUnauthorizedResponse({ - description: 'If access token is missing or invalid, or policy does not belong to user', - }) - @ApiInternalServerErrorResponse({ description: 'If failed to delete rule' }) - @UseGuards(BelongingInlinePolicyGuard) - @Delete(Routes.REMOVE_RULE) - @HttpCode(HttpStatus.OK) - public async removeRule( - @Param('policy_id') policyId: string, - @Body() removeRuleDto: RemoveRuleDto, - ): Promise { - this.logger.log( - `Will try to remove rule "${removeRuleDto.ruleName}" for an inline policy with an Id: "${policyId}"`, - ); - - await this.commandBus.execute( - new RemoveRuleContract(policyId, removeRuleDto.ruleName), - ); - - this.logger.log( - `Successfully removed a rule "${removeRuleDto.ruleName}" for an inline policy Id: "${policyId}"`, - ); - } -} diff --git a/apps/backend/src/modules/user/modules/inline-policies/update-configuration.controller.ts b/apps/backend/src/modules/user/modules/inline-policies/update-configuration.controller.ts deleted file mode 100644 index 27adcdd12..000000000 --- a/apps/backend/src/modules/user/modules/inline-policies/update-configuration.controller.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Body, Controller, HttpCode, HttpStatus, Logger, Param, Patch, UseGuards } from '@nestjs/common'; -import { CommandBus } from '@nestjs/cqrs'; -import { - ApiBearerAuth, - ApiInternalServerErrorResponse, - ApiOkResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; - -import Routes from './inline-policies.routes'; -import { BelongingInlinePolicyGuard } from './guards/belonging-inline-policy.guard'; -import { UpdateConfigurationDto } from './classes/update-configuration.dto'; -import { UpdateConfigurationContract } from './commands/contracts/update-configuration.contract'; - -@ApiTags('Inline Policies') -@Controller(Routes.CONTROLLER) -export class UpdateConfigurationController { - private readonly logger = new Logger(UpdateConfigurationController.name); - - constructor(private readonly commandBus: CommandBus) {} - - @ApiOperation({ description: 'Update the configuration of a policy by its identifier' }) - @ApiBearerAuth('access-token') - @ApiOkResponse({ description: 'If successfully update configuration of policy' }) - @ApiUnauthorizedResponse({ - description: 'If access token is missing or invalid, or policy does not belong to user', - }) - @ApiInternalServerErrorResponse({ description: 'If failed to update policy configuration' }) - @UseGuards(BelongingInlinePolicyGuard) - @Patch(Routes.UPDATE_CONFIGURATION) - @HttpCode(HttpStatus.OK) - public async updateConfiguration( - @Param('policy_id') policyId: string, - @Body() updateConfigurationDto: UpdateConfigurationDto, - ): Promise { - this.logger.log(`Will try to update configuration for an inline policy with an Id: "${policyId}"`); - - await this.commandBus.execute( - new UpdateConfigurationContract(policyId, updateConfigurationDto.configuration), - ); - - this.logger.log(`Successfully updated a configuration for an inline policy Id: "${policyId}"`); - } -} diff --git a/apps/backend/tsconfig.base.json b/apps/backend/tsconfig.base.json index 349df1fd1..6540320ae 100644 --- a/apps/backend/tsconfig.base.json +++ b/apps/backend/tsconfig.base.json @@ -11,7 +11,8 @@ "@/decorators/*": ["src/decorators/*"], "@/models/*": ["src/models/*"], "@/interfaces/*": ["src/interfaces/*"], - "@/guards/*": ["src/guards/*"] + "@/guards/*": ["src/guards/*"], + "@/data/*": ["src/data/*"] }, "typeRoots": ["./node_modules/@types", "./@types"] } diff --git a/apps/frontend/.eslintrc.cjs b/apps/frontend/.eslintrc.cjs index 14892258b..2e2e800d4 100644 --- a/apps/frontend/.eslintrc.cjs +++ b/apps/frontend/.eslintrc.cjs @@ -30,6 +30,7 @@ module.exports = { 'jsx-a11y/lang': 'error', 'jsx-a11y/no-redundant-roles': 'error', + 'react/jsx-indent': 'off', 'react/jsx-fragments': 'error', 'react/jsx-wrap-multilines': [ 'error', diff --git a/apps/frontend/src/App.router.tsx b/apps/frontend/src/App.router.tsx index 6bd37b91c..0dff98b42 100644 --- a/apps/frontend/src/App.router.tsx +++ b/apps/frontend/src/App.router.tsx @@ -20,6 +20,7 @@ const CliAuth = React.lazy(() => import('./pages/CliAuth')); const CliAuthenticated = React.lazy(() => import('./pages/CliAuthenticated')); const NotFound = React.lazy(() => import('./pages/NotFound')); const GroupCenter = React.lazy(() => import('./pages/GroupCenter')); +const NewPolicy = React.lazy(() => import('./pages/NewPolicy')); const AppRouter: React.FC = (props: React.PropsWithChildren) => ( @@ -50,6 +51,7 @@ const AppRouter: React.FC = (props: React.PropsWithChildren) => } /> + } /> )} } /> diff --git a/apps/frontend/src/assets/icons.ts b/apps/frontend/src/assets/icons.ts index e1879f0e8..154973e3f 100644 --- a/apps/frontend/src/assets/icons.ts +++ b/apps/frontend/src/assets/icons.ts @@ -147,6 +147,12 @@ const icons = { `, ], + search: [ + '16 16', + ` + + `, + ], }; export default icons; diff --git a/apps/frontend/src/components/containers/AccountSettings/NewSecret/NewSecret.tsx b/apps/frontend/src/components/containers/AccountSettings/NewSecret/NewSecret.tsx index d7767be9a..e0ccd5d79 100644 --- a/apps/frontend/src/components/containers/AccountSettings/NewSecret/NewSecret.tsx +++ b/apps/frontend/src/components/containers/AccountSettings/NewSecret/NewSecret.tsx @@ -3,8 +3,9 @@ import { useNavigate } from 'react-router-dom'; import { useDebounce } from '@/hooks/use-debounce'; import { backendApi } from '@/utils/http'; +import type { IAvailableLabelResponse } from '@/interfaces/responses'; -import type { IAvailableLabelResponse, ICreateSecretResponse } from './interfaces/responses'; +import type { ICreateSecretResponse } from './interfaces/responses'; import { WEEK_INTERVAL } from './models/time'; import NewSecretView from './NewSecret.view'; diff --git a/apps/frontend/src/components/containers/AccountSettings/NewSecret/interfaces/responses.ts b/apps/frontend/src/components/containers/AccountSettings/NewSecret/interfaces/responses.ts index 8635abe01..6915d02d3 100644 --- a/apps/frontend/src/components/containers/AccountSettings/NewSecret/interfaces/responses.ts +++ b/apps/frontend/src/components/containers/AccountSettings/NewSecret/interfaces/responses.ts @@ -1,7 +1,3 @@ -export interface IAvailableLabelResponse { - readonly isAvailable: boolean; -} - export interface ICreateSecretResponse { readonly secretId: string; readonly secretValue: string; diff --git a/apps/frontend/src/components/containers/GroupCenter/GroupDetails/GroupDetails.tsx b/apps/frontend/src/components/containers/GroupCenter/GroupDetails/GroupDetails.tsx index 31e80580b..f2bbe8343 100644 --- a/apps/frontend/src/components/containers/GroupCenter/GroupDetails/GroupDetails.tsx +++ b/apps/frontend/src/components/containers/GroupCenter/GroupDetails/GroupDetails.tsx @@ -11,8 +11,7 @@ import type { } from '@/store/interfaces/groups'; import { groupsActions } from '@/store/reducers/groups'; import type { AppState } from '@/store/app'; - -import type { IGetGroupResponse } from './interfaces/group'; +import type { IGetGroupResponse } from '@/interfaces/responses'; import GroupDetailsView from './GroupDetails.view'; diff --git a/apps/frontend/src/components/containers/GroupCenter/GroupDetails/Settings/EditGroupLabel/EditGroupLabel.tsx b/apps/frontend/src/components/containers/GroupCenter/GroupDetails/Settings/EditGroupLabel/EditGroupLabel.tsx index 40f2d9c71..c3135a8ee 100644 --- a/apps/frontend/src/components/containers/GroupCenter/GroupDetails/Settings/EditGroupLabel/EditGroupLabel.tsx +++ b/apps/frontend/src/components/containers/GroupCenter/GroupDetails/Settings/EditGroupLabel/EditGroupLabel.tsx @@ -2,8 +2,7 @@ import React, { useEffect, useState } from 'react'; import { useDebounce } from '@/hooks/use-debounce'; import { backendApi } from '@/utils/http'; - -import type { IAvailableLabelResponse } from './interfaces/response'; +import type { IAvailableLabelResponse } from '@/interfaces/responses'; import EditGroupLabelView from './EditGroupLabel.view'; diff --git a/apps/frontend/src/components/containers/GroupCenter/GroupDetails/Settings/EditGroupLabel/interfaces/response.ts b/apps/frontend/src/components/containers/GroupCenter/GroupDetails/Settings/EditGroupLabel/interfaces/response.ts deleted file mode 100644 index 6f32e520f..000000000 --- a/apps/frontend/src/components/containers/GroupCenter/GroupDetails/Settings/EditGroupLabel/interfaces/response.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface IAvailableLabelResponse { - readonly isAvailable: boolean; -} diff --git a/apps/frontend/src/components/containers/GroupCenter/GroupDetails/interfaces/group.ts b/apps/frontend/src/components/containers/GroupCenter/GroupDetails/interfaces/group.ts deleted file mode 100644 index 1a5a7e849..000000000 --- a/apps/frontend/src/components/containers/GroupCenter/GroupDetails/interfaces/group.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface IGetGroupResponse { - readonly label: string; -} diff --git a/apps/frontend/src/components/containers/GroupCenter/NewGroup/NewGroup.tsx b/apps/frontend/src/components/containers/GroupCenter/NewGroup/NewGroup.tsx index 1412fc174..5f722d5ff 100644 --- a/apps/frontend/src/components/containers/GroupCenter/NewGroup/NewGroup.tsx +++ b/apps/frontend/src/components/containers/GroupCenter/NewGroup/NewGroup.tsx @@ -8,8 +8,9 @@ import { useDebounce } from '@/hooks/use-debounce'; import { backendApi } from '@/utils/http'; import { groupsActions } from '@/store/reducers/groups'; import type { IAddSideBarGroupsPayload } from '@/store/interfaces/groups'; +import type { IAvailableLabelResponse } from '@/interfaces/responses'; -import type { IAvailableLabelResponse, ICreateGroupResponse } from './interfaces/response'; +import type { ICreateGroupResponse } from './interfaces/response'; import NewGroupView from './NewGroup.view'; @@ -41,8 +42,8 @@ const NewGroup: React.FC = (props: React.PropsWithChildren) => { backendApi .get(`/user/groups/available/${groupLabelInputState}`) .then((response) => { - setIsGroupLabelValidState(response.data.isAvailable); - setIsGroupLabelAvailableState(response.data.isAvailable); + setIsGroupLabelValidState(() => response.data.isAvailable); + setIsGroupLabelAvailableState(() => response.data.isAvailable); }); } }, diff --git a/apps/frontend/src/components/containers/GroupCenter/NewGroup/interfaces/response.ts b/apps/frontend/src/components/containers/GroupCenter/NewGroup/interfaces/response.ts index 45900e8ea..312297a38 100644 --- a/apps/frontend/src/components/containers/GroupCenter/NewGroup/interfaces/response.ts +++ b/apps/frontend/src/components/containers/GroupCenter/NewGroup/interfaces/response.ts @@ -1,7 +1,3 @@ -export interface IAvailableLabelResponse { - readonly isAvailable: boolean; -} - export interface ICreateGroupResponse { readonly groupId: string; } diff --git a/apps/frontend/src/components/containers/NewPolicy/Details/Details.module.scss b/apps/frontend/src/components/containers/NewPolicy/Details/Details.module.scss new file mode 100644 index 000000000..8d468c935 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/Details/Details.module.scss @@ -0,0 +1,74 @@ +@use 'sass:map'; + +@import '../../../../styles/variables.scss'; + +.container { + display: flex; + flex-direction: column; + padding-top: map.get($sizes, spacing-xxl-2); + + &__header { + font-size: 2.4rem; + font-weight: normal; + color: map.get($colors, greys-onyx); + } + + &__subHeader { + margin-top: map.get($sizes, spacing-s); + font-size: 1.7rem; + color: map.get($colors, greys-independence-grey); + } + + &__divider { + width: 708px; + height: 2px; + margin-top: map.get($sizes, spacing-xl); + background-color: map.get($colors, greys-platinum); + } + + .labelTable { + width: min-content; + margin-block: map.get($sizes, spacing-xl) map.get($sizes, spacing-l); + border-spacing: map.get($sizes, spacing-m) map.get($sizes, spacing-s); + + &__header { + font-size: 1.7rem; + font-weight: 500; + color: map.get($colors, greys-onyx); + text-align: start; + + &--inputlabel { + width: 259px; + } + } + + .groupLabel { + display: flex; + align-items: center; + justify-content: space-between; + + &__value { + margin-inline-end: map.get($sizes, spacing-s); + font-size: 1.7rem; + color: map.get($colors, greys-onyx); + } + + &__postfix { + font-size: 2.1rem; + font-weight: 500; + color: map.get($colors, greys-onyx); + } + } + } + + &__descriptionLabel { + margin-bottom: map.get($sizes, spacing-m); + font-size: 1.7rem; + font-weight: 500; + color: map.get($colors, greys-independence-grey); + + &--postfix { + font-size: 1.5rem; + } + } +} diff --git a/apps/frontend/src/components/containers/NewPolicy/Details/Details.tsx b/apps/frontend/src/components/containers/NewPolicy/Details/Details.tsx new file mode 100644 index 000000000..523cf1eb6 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/Details/Details.tsx @@ -0,0 +1,83 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; + +import { backendApi } from '@/utils/http'; +import type { IAvailableLabelResponse, IGetGroupResponse } from '@/interfaces/responses'; +import { useDebounce } from '@/hooks/use-debounce'; + +import DetailsView from './Details.view'; + +interface IProps { + readonly policyLabel: string | null; + readonly policyDescription: string | null; + readonly isPolicyLabelAvailable: boolean | null; + readonly onPolicyLabelChange: (value: string) => void; + readonly onPolicyDescriptionChange: (value: string) => void; + readonly onSetPolicyLabelValid: (value: boolean) => void; + readonly onSetPolicyLabelAvailable: (value: boolean | null) => void; +} + +const Details: React.FC = (props: React.PropsWithChildren) => { + const [groupLabelState, setGroupLabelState] = useState(null); + + const params = useParams<{ readonly groupId: string }>(); + const navigate = useNavigate(); + + useEffect(() => { + if (!params.groupId) { + navigate('/not-found'); + + return; + } + + backendApi + .get(`/user/groups/${params.groupId}`) + .then((response) => setGroupLabelState(() => response.data.label)) + .catch(() => navigate('/')); + }, [params.groupId, backendApi]); + + useEffect(() => { + if (props.policyLabel === '' || props.policyLabel === null) { + props.onSetPolicyLabelValid(false); + } + }, [props.policyLabel]); + + useDebounce( + () => { + if (props.policyLabel === '' || props.policyLabel === null) { + props.onSetPolicyLabelValid(false); + } else { + backendApi + .get(`/user/inline-policies/available/${props.policyLabel}`) + .then((response) => { + props.onSetPolicyLabelAvailable(response.data.isAvailable); + props.onSetPolicyLabelValid(response.data.isAvailable); + }); + } + }, + [props.policyLabel], + 400, + ); + + const onPolicyLabelChange = (value: string) => { + props.onPolicyLabelChange(value); + props.onSetPolicyLabelValid(false); + props.onSetPolicyLabelAvailable(null); + }; + + return ( + + ); +}; + +Details.displayName = 'Details'; +Details.defaultProps = {}; + +export default React.memo(Details); diff --git a/apps/frontend/src/components/containers/NewPolicy/Details/Details.view.tsx b/apps/frontend/src/components/containers/NewPolicy/Details/Details.view.tsx new file mode 100644 index 000000000..9bf3b6682 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/Details/Details.view.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { concatClasses } from '@/utils/component'; +import EDInputFieldAvailability from '@/ui/EDInputFieldAvailability'; +import EDInputField from '@/ui/EDInputField'; + +import classes from './Details.module.scss'; + +interface IProps { + readonly selectedGroupLabel: string | null; + readonly policyLabel: string | null; + readonly isPolicyLabelAvailable: boolean | null; + readonly policyDescritpion: string | null; + readonly onPolicyLabelChange: (value: string) => void; + readonly onPolicyDescriptionChange: (value: string) => void; +} + +const DetailsView: React.FC = (props: React.PropsWithChildren) => { + const { t } = useTranslation(); + + return ( +
+

{t('newPolicy.formHeader')}

+ {t('newPolicy.formSubHeader')} + +
+ + + + + + + + + + + + + + + +
{t('newPolicy.group')} + {t('newPolicy.policyLabel')} +
+ + + {props.selectedGroupLabel} + + / + + + +
+ + + + +
+
+ ); +}; + +DetailsView.displayName = 'DetailsView'; +DetailsView.defaultProps = {}; + +export default React.memo(DetailsView); diff --git a/apps/frontend/src/components/containers/NewPolicy/Details/index.ts b/apps/frontend/src/components/containers/NewPolicy/Details/index.ts new file mode 100644 index 000000000..2c1372e2d --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/Details/index.ts @@ -0,0 +1,3 @@ +import Details from './Details'; + +export default Details; diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrariesList/LibrariesList.module.scss b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrariesList/LibrariesList.module.scss new file mode 100644 index 000000000..b249da98a --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrariesList/LibrariesList.module.scss @@ -0,0 +1,127 @@ +@use 'sass:map'; + +@import '../../../../../styles/variables.scss'; + +.container { + display: grid; + grid-template-columns: 50% 50%; + grid-auto-rows: min-content; + gap: map.get($sizes, spacing-xxl); + width: 100%; + margin-inline-start: map.get($sizes, spacing-xxl-2); +} + +.libraryItem { + display: flex; + cursor: pointer; + + .libraryImgBorder { + display: flex; + align-items: center; + justify-content: center; + width: 53px; + min-width: 53px; + height: 53px; + margin-inline-end: map.get($sizes, spacing-m); + background: transparent; + border-radius: 50%; + + &--selected { + background: linear-gradient(270deg, #dc6794 -28.49%, #9747ff 83.02%), + linear-gradient(0deg, #fefefe, #fefefe); + } + } + + .libraryImgContainer { + display: flex; + align-items: center; + justify-content: center; + width: 49px; + min-width: 49px; + height: 49px; + background-color: map.get($colors, whites-white); + border-radius: 50%; + box-shadow: 0 4px 8px 0 #00000026; + + &__img { + width: 30px; + height: 30px; + object-fit: contain; + } + } + + .libraryDetails { + display: flex; + flex-direction: column; + + .libraryNameContainer { + display: flex; + align-items: center; + justify-content: space-between; + border-bottom-style: solid; + border-bottom-width: 2px; + border-image: linear-gradient(270deg, #dc6794 -28.49%, #9747ff 83.02%) 30; + + &__name { + font-size: 1.7rem; + font-weight: 600; + background: -webkit-linear-gradient(270deg, #dc6794 -28.49%, #9747ff 83.02%); + background-clip: text; + -webkit-text-fill-color: transparent; + } + + .libraryNameAction { + display: flex; + + &__text { + margin-inline-end: map.get($sizes, spacing-s); + font-size: 1.5rem; + font-weight: 500; + color: map.get($colors, greys-independence-grey); + } + + &__icon { + width: 10px; + height: auto; + fill: map.get($colors, greys-independence-grey); + } + } + } + + &__name { + font-size: 1.7rem; + font-weight: 600; + color: map.get($colors, purples-purple); + border-bottom: 2px solid transparent; + } + + &__author { + margin-block: map.get($sizes, spacing-s); + font-size: 1.5rem; + color: map.get($colors, greys-independence-grey); + } + + &__description { + font-size: 1.5rem; + font-weight: 300; + color: map.get($colors, greys-independence-grey); + } + + .libraryCategorization { + display: flex; + margin-top: map.get($sizes, spacing-s); + + &__item { + padding: map.get($sizes, spacing-xs) map.get($sizes, spacing-m); + font-size: 1.3rem; + color: map.get($colors, greys-independence-grey); + border: 1px solid map.get($colors, greys-silver-sand); + border-radius: 10px; + + &:not(:last-child) { + margin-inline-end: map.get($sizes, spacing-s); + } + } + } + } +} diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrariesList/LibrariesList.tsx b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrariesList/LibrariesList.tsx new file mode 100644 index 000000000..5bfee60dc --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrariesList/LibrariesList.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +import type { ILibraryName } from '@/interfaces/libraries'; + +import type { ILibrary } from '../interfaces/library'; + +import LibrariesListView from './LibrariesList.view'; + +interface IProps { + readonly selectedLibrary: ILibraryName | null; + readonly libraries: ILibrary[]; + readonly onLibrarySelect: (library: ILibraryName) => void; + readonly onLibraryDeselect: VoidFunction; +} + +const LibrariesList: React.FC = (props: React.PropsWithChildren) => { + return ( + + ); +}; + +LibrariesList.displayName = 'LibrariesList'; +LibrariesList.defaultProps = {}; + +export default React.memo(LibrariesList); diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrariesList/LibrariesList.view.tsx b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrariesList/LibrariesList.view.tsx new file mode 100644 index 000000000..abf981bef --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrariesList/LibrariesList.view.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import logosObject from '@/utils/libraries-logos'; +import type { ILibraryName } from '@/interfaces/libraries'; +import { concatClasses } from '@/utils/component'; +import EDSvg from '@/ui/EDSvg'; + +import type { ILibrary } from '../interfaces/library'; + +import classes from './LibrariesList.module.scss'; + +interface IProps { + readonly selectedLibrary: ILibraryName | null; + readonly libraries: ILibrary[]; + readonly onLibrarySelect: (library: ILibraryName) => void; + readonly onLibraryDeselect: VoidFunction; +} + +const LibrariesListView: React.FC = (props: React.PropsWithChildren) => { + const { t } = useTranslation(); + + return ( +
+ {props.libraries.map((library, index) => { + const libraryNameLowerCase = library.name.toLowerCase() as keyof typeof logosObject; + const isSelected = props.selectedLibrary === library.name; + + return ( +
props.onLibrarySelect(library.name)} + > +
+
+ {library.name} +
+
+ +
+ {isSelected ? ( +
+
{library.name}
+ +
+ + {t('newPolicy.librarySelection.selected')} + + +
+
+ ) : ( +
{library.name}
+ )} + + {library.author} + + {library.description} + +
+ {library.types.map((libraryType, index) => ( + + {libraryType} + + ))} + {library.categories.map((libraryCategory, index) => ( + + {libraryCategory} + + ))} +
+
+
+ ); + })} +
+ ); +}; + +LibrariesListView.displayName = 'LibrariesListView'; +LibrariesListView.defaultProps = {}; + +export default React.memo(LibrariesListView); diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrariesList/index.ts b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrariesList/index.ts new file mode 100644 index 000000000..797c47452 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrariesList/index.ts @@ -0,0 +1,3 @@ +import LibrariesList from './LibrariesList'; + +export default LibrariesList; diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrarySelection.module.scss b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrarySelection.module.scss new file mode 100644 index 000000000..3a5b46a86 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrarySelection.module.scss @@ -0,0 +1,38 @@ +@use 'sass:map'; + +@import '../../../../styles/variables.scss'; + +.container { + display: flex; + flex: 1; + flex-direction: column; + width: 100%; + + &__header { + margin-bottom: map.get($sizes, spacing-m); + font-size: 1.7rem; + font-weight: 500; + color: map.get($colors, greys-onyx); + } + + .filtersContainer { + display: flex; + + &__input { + width: 493px; + margin-inline-end: map.get($sizes, spacing-l); + background-color: map.get($colors, whites-white); + border: 2px solid map.get($colors, greys-platinum); + box-shadow: 0 10px 20px 0 #00000040; + } + + &__select { + box-shadow: 0 10px 20px 0 #00000040; + } + } +} + +.filtersAndLibraries { + display: flex; + margin-top: map.get($sizes, spacing-xxl); +} diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrarySelection.tsx b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrarySelection.tsx new file mode 100644 index 000000000..f2c709702 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrarySelection.tsx @@ -0,0 +1,116 @@ +import { useParams } from 'react-router-dom'; +import React, { useEffect, useState } from 'react'; + +import type { ILibraryName } from '@/interfaces/libraries'; +import { backendApi } from '@/utils/http'; + +import type { ICategoryFilter, ILanguageFilter, ITypeFilter } from './interfaces/type-filters'; +import type { ILibrary } from './interfaces/library'; +import type { IGetLibrariesResponse } from './interfaces/responses'; + +import LibrarySelectionView from './LibrarySelection.view'; + +interface IProps { + readonly selectedLibrary: ILibraryName | null; + readonly onLibrarySelect: (library: ILibraryName | null) => void; +} + +const LibrarySelection: React.FC = (props: React.PropsWithChildren) => { + const [libraryFilterInputState, setLibraryFilterInputState] = useState(null); + const [selectedSortIndexState, setSelectedSortIndexState] = useState(0); + const [languageFilterState, setLanguageFilterState] = useState('All'); + const [typeFilterState, setTypeFilterState] = useState('All'); + const [categoryFilterState, setCategoryFilterState] = useState('All'); + const [librariesState, setLibrariesState] = useState([]); + const [filteredLibrariesState, setFilteredLibrariesState] = useState([]); + + const params = useParams<{ readonly groupId: string }>(); + + useEffect(() => { + backendApi + .get(`/user/inline-policies/libraries/${params.groupId}`) + .then((response) => { + setLibrariesState(() => response.data.libraries); + setFilteredLibrariesState(() => response.data.libraries); + }); + }, [backendApi, params.groupId]); + + useEffect(() => { + setFilteredLibrariesState(() => { + const lowerCaseFilter = libraryFilterInputState?.toLowerCase(); + + return librariesState + .filter((library) => { + const isMatchingByInputFilter = + lowerCaseFilter === undefined || + lowerCaseFilter === '' || + library.name.toLowerCase().includes(lowerCaseFilter) || + library.description.toLowerCase().includes(lowerCaseFilter) || + library.categories.some((category) => + category.toLowerCase().includes(lowerCaseFilter), + ); + + const isMatchingByLanguageFilter = + languageFilterState === 'All' || languageFilterState === library.language; + + const isMatchingTypesFilter = + typeFilterState === 'All' || library.types.includes(typeFilterState); + + const isMatchingCategoriesFilter = + categoryFilterState === 'All' || library.categories.includes(categoryFilterState); + + return ( + isMatchingByLanguageFilter && + isMatchingByInputFilter && + isMatchingTypesFilter && + isMatchingCategoriesFilter + ); + }) + .sort((lib1, lib2) => { + if (selectedSortIndexState === 0) { + return lib1.name.localeCompare(lib2.name); + } else { + return lib1.language.localeCompare(lib2.language); + } + }); + }); + }, [ + libraryFilterInputState, + languageFilterState, + typeFilterState, + categoryFilterState, + selectedSortIndexState, + ]); + + const onLibraryDeselect = () => props.onLibrarySelect(null); + + const onLibraryFilterInputChange = (value: string) => setLibraryFilterInputState(() => value); + const onSelectSortOption = (index: number) => setSelectedSortIndexState(() => index); + const onSetLanguageFilter = (value: ILanguageFilter) => setLanguageFilterState(() => value); + const onSetTypeFilter = (value: ITypeFilter) => setTypeFilterState(() => value); + const onSetCategoryFilter = (value: ICategoryFilter) => setCategoryFilterState(() => value); + + return ( + + ); +}; + +LibrarySelection.displayName = 'LibrarySelection'; +LibrarySelection.defaultProps = {}; + +export default React.memo(LibrarySelection); diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrarySelection.view.tsx b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrarySelection.view.tsx new file mode 100644 index 000000000..2da29ed74 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/LibrarySelection.view.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import EDInputField from '@/ui/EDInputField'; +import EDSelect from '@/ui/EDSelect'; +import type { IOption } from '@/ui/EDSelect/interfaces/option'; +import type { ILibraryName } from '@/interfaces/libraries'; + +import type { ICategoryFilter, ILanguageFilter, ITypeFilter } from './interfaces/type-filters'; +import type { ISortOption } from './interfaces/sort-option'; +import SideFilters from './SideFilters'; +import LibrariesList from './LibrariesList'; +import type { ILibrary } from './interfaces/library'; + +import classes from './LibrarySelection.module.scss'; + +interface IProps { + readonly libraryFilterInput: string | null; + readonly selectedSortIndex: number; + readonly languageFilter: ILanguageFilter; + readonly typeFilter: ITypeFilter; + readonly categoryFilter: ICategoryFilter; + readonly selectedLibrary: ILibraryName | null; + readonly libraries: ILibrary[]; + readonly onLibraryFilterInputChange: (value: string) => void; + readonly onSelectSortOption: (index: number) => void; + readonly onSetLanguageFilter: (value: ILanguageFilter) => void; + readonly onSetTypeFilter: (value: ITypeFilter) => void; + readonly onSetCategoryFilter: (value: ICategoryFilter) => void; + readonly onLibrarySelect: (library: ILibraryName) => void; + readonly onLibraryDeselect: VoidFunction; +} + +const LibrarySelectionView: React.FC = (props: React.PropsWithChildren) => { + const { t } = useTranslation(); + + const selectOptions: IOption[] = [ + { + value: 'Alphabetic', + label: 'A-Z', + }, + { + value: 'Language', + label: t('newPolicy.librarySelection.languageSort'), + }, + ]; + + return ( +
+
{t('newPolicy.librarySelection.header')}
+ +
+ + + +
+ +
+ + + +
+
+ ); +}; + +LibrarySelectionView.displayName = 'LibrarySelectionView'; +LibrarySelectionView.defaultProps = {}; + +export default React.memo(LibrarySelectionView); diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/Filter/Filter.module.scss b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/Filter/Filter.module.scss new file mode 100644 index 000000000..4bcee433d --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/Filter/Filter.module.scss @@ -0,0 +1,34 @@ +@use 'sass:map'; + +@import '../../../../../../styles/variables.scss'; + +.container { + display: flex; + flex-direction: column; + + &:not(:last-child) { + margin-bottom: map.get($sizes, spacing-xxl); + } + + &__title { + margin-bottom: map.get($sizes, spacing-l); + font-size: 2.1rem; + font-weight: 500; + color: map.get($colors, greys-independence-grey); + } + + &__option { + font-size: 1.7rem; + color: map.get($colors, greys-independence-grey); + text-align: start; + + &:not(:last-child) { + margin-bottom: map.get($sizes, spacing-m); + } + + &--selected { + font-weight: 500; + color: map.get($colors, purples-purple); + } + } +} diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/Filter/Filter.tsx b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/Filter/Filter.tsx new file mode 100644 index 000000000..b28fe0025 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/Filter/Filter.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import type { ICategoryFilter, ILanguageFilter, ITypeFilter } from '../../interfaces/type-filters'; +import type { IOption } from '../interfaces/option'; + +import FilterView from './Filter.view'; + +declare module 'react' { + function memo(Component: (props: A) => B): (props: A) => React.ReactElement | null; +} + +interface IProps { + readonly title: string; + readonly options: IOption[]; + readonly selectedOption: T; + readonly onSelectOption: (value: T) => void; +} + +const Filter = ( + props: React.PropsWithChildren>, +) => { + return ( + + ); +}; + +Filter.displayName = 'Filter'; +Filter.defaultProps = {}; + +export default React.memo(Filter); diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/Filter/Filter.view.tsx b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/Filter/Filter.view.tsx new file mode 100644 index 000000000..146d0480e --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/Filter/Filter.view.tsx @@ -0,0 +1,45 @@ +import React from 'react'; + +import { concatClasses } from '@/utils/component'; + +import type { IOption } from '../interfaces/option'; +import type { ICategoryFilter, ILanguageFilter, ITypeFilter } from '../../interfaces/type-filters'; + +import classes from './Filter.module.scss'; + +interface IProps { + readonly title: string; + readonly options: IOption[]; + readonly selectedOption: T; + readonly onSelectOption: (value: T) => void; +} + +const FilterView = ( + props: React.PropsWithChildren>, +) => { + return ( +
+
{props.title}
+ + {props.options.map((option, index) => ( + + ))} +
+ ); +}; + +FilterView.displayName = 'FilterView'; +FilterView.defaultProps = {}; + +export default React.memo(FilterView); diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/Filter/index.ts b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/Filter/index.ts new file mode 100644 index 000000000..6ebc41c0e --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/Filter/index.ts @@ -0,0 +1,3 @@ +import Filter from './Filter'; + +export default Filter; diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/SideFilters.module.scss b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/SideFilters.module.scss new file mode 100644 index 000000000..e2a893168 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/SideFilters.module.scss @@ -0,0 +1,4 @@ +.container { + display: flex; + flex-direction: column; +} diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/SideFilters.tsx b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/SideFilters.tsx new file mode 100644 index 000000000..42ab03c82 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/SideFilters.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import type { ICategoryFilter, ILanguageFilter, ITypeFilter } from '../interfaces/type-filters'; + +import SideFiltersView from './SideFilters.view'; + +interface IProps { + readonly languageFilter: ILanguageFilter; + readonly typeFilter: ITypeFilter; + readonly categoryFilter: ICategoryFilter; + readonly onSetLanguageFilter: (value: ILanguageFilter) => void; + readonly onSetTypeFilter: (value: ITypeFilter) => void; + readonly onSetCategoryFilter: (value: ICategoryFilter) => void; +} + +const SideFilters: React.FC = (props: React.PropsWithChildren) => { + return ( + + ); +}; + +SideFilters.displayName = 'SideFilters'; +SideFilters.defaultProps = {}; + +export default React.memo(SideFilters); diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/SideFilters.view.tsx b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/SideFilters.view.tsx new file mode 100644 index 000000000..f1226ff5a --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/SideFilters.view.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { ICategoryFilter, ILanguageFilter, ITypeFilter } from '../interfaces/type-filters'; +import type { IOption } from './interfaces/option'; + +import Filter from './Filter'; + +import classes from './SideFilters.module.scss'; + +interface IProps { + readonly languageFilter: ILanguageFilter; + readonly typeFilter: ITypeFilter; + readonly categoryFilter: ICategoryFilter; + readonly onSetLanguageFilter: (value: ILanguageFilter) => void; + readonly onSetTypeFilter: (value: ITypeFilter) => void; + readonly onSetCategoryFilter: (value: ICategoryFilter) => void; +} + +const SideFiltersView: React.FC = (props: React.PropsWithChildren) => { + const { t } = useTranslation(); + + const languageOptions: IOption[] = [ + { value: 'All', label: t('newPolicy.librarySelection.filters.all') }, + { value: 'JavaScript', label: 'JavaScript' }, + { value: 'CSSHTML', label: 'CSS/HTML' }, + { value: 'Agnostic', label: t('newPolicy.librarySelection.filters.agnostic') }, + ]; + + const typeOptions: IOption[] = [ + { value: 'All', label: t('newPolicy.librarySelection.filters.all') }, + { value: 'Linters', label: t('newPolicy.librarySelection.filters.linters') }, + { value: 'Formatters', label: t('newPolicy.librarySelection.filters.formatters') }, + ]; + + const categoryOptions: IOption[] = [ + { value: 'All', label: t('newPolicy.librarySelection.filters.all') }, + { value: 'Code', label: t('newPolicy.librarySelection.filters.code') }, + { value: 'File System', label: t('newPolicy.librarySelection.filters.fileSystem') }, + { value: 'Styles', label: t('newPolicy.librarySelection.filters.styles') }, + { value: 'Dependencies', label: t('newPolicy.librarySelection.filters.dependencies') }, + ]; + + return ( +
+ + + +
+ ); +}; + +SideFiltersView.displayName = 'SideFiltersView'; +SideFiltersView.defaultProps = {}; + +export default React.memo(SideFiltersView); diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/index.ts b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/index.ts new file mode 100644 index 000000000..ba492629e --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/index.ts @@ -0,0 +1,3 @@ +import SideFilters from './SideFilters'; + +export default SideFilters; diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/interfaces/option.ts b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/interfaces/option.ts new file mode 100644 index 000000000..489927c26 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/SideFilters/interfaces/option.ts @@ -0,0 +1,4 @@ +export interface IOption { + readonly value: T; + readonly label: string; +} diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/index.ts b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/index.ts new file mode 100644 index 000000000..8c243ecbb --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/index.ts @@ -0,0 +1,3 @@ +import LibrarySelection from './LibrarySelection'; + +export default LibrarySelection; diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/interfaces/library.ts b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/interfaces/library.ts new file mode 100644 index 000000000..bfa210c03 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/interfaces/library.ts @@ -0,0 +1,12 @@ +import type { ILibraryName } from '@/interfaces/libraries'; + +import type { ICategory, ILanguage, IType } from './type-filters'; + +export interface ILibrary { + readonly name: ILibraryName; + readonly author: string; + readonly description: string; + readonly types: IType[]; + readonly categories: ICategory[]; + readonly language: ILanguage; +} diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/interfaces/responses.ts b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/interfaces/responses.ts new file mode 100644 index 000000000..eb107a3a4 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/interfaces/responses.ts @@ -0,0 +1,5 @@ +import type { ILibrary } from './library'; + +export interface IGetLibrariesResponse { + readonly libraries: ILibrary[]; +} diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/interfaces/sort-option.ts b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/interfaces/sort-option.ts new file mode 100644 index 000000000..d8898d5cc --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/interfaces/sort-option.ts @@ -0,0 +1 @@ +export type ISortOption = 'Alphabetic' | 'Language'; diff --git a/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/interfaces/type-filters.ts b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/interfaces/type-filters.ts new file mode 100644 index 000000000..7c87f6c25 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/LibrarySelection/interfaces/type-filters.ts @@ -0,0 +1,11 @@ +export type ILanguage = 'JavaScript' | 'CSSHTML' | 'Agnostic'; + +export type IType = 'Linters' | 'Formatters'; + +export type ICategory = 'Code' | 'File System' | 'Styles' | 'Dependencies'; + +export type ILanguageFilter = 'All' | ILanguage; + +export type ITypeFilter = 'All' | IType; + +export type ICategoryFilter = 'All' | ICategory; diff --git a/apps/frontend/src/components/containers/NewPolicy/NewPolicy.module.scss b/apps/frontend/src/components/containers/NewPolicy/NewPolicy.module.scss new file mode 100644 index 000000000..41a841411 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/NewPolicy.module.scss @@ -0,0 +1,33 @@ +@use 'sass:map'; + +@import '../../../styles/variables.scss'; + +.container { + display: flex; + flex-direction: column; + height: 100%; +} + +.form { + display: flex; + flex: 1; + flex-direction: column; + align-items: center; + + .formFooter { + width: 100%; + padding-block: map.get($sizes, spacing-xl) map.get($sizes, spacing-xxl-4); + background-color: map.get($colors, whites-ghost-white); + + .formFooterInner { + width: 708px; + margin-inline: auto; + + &__divider { + height: 2px; + margin-block: map.get($sizes, spacing-xl); + background-color: map.get($colors, greys-platinum); + } + } + } +} diff --git a/apps/frontend/src/components/containers/NewPolicy/NewPolicy.tsx b/apps/frontend/src/components/containers/NewPolicy/NewPolicy.tsx new file mode 100644 index 000000000..f7dffc5f2 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/NewPolicy.tsx @@ -0,0 +1,67 @@ +import React, { type FormEvent, useMemo, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; + +import type { ILibraryName } from '@/interfaces/libraries'; +import { backendApi } from '@/utils/http'; + +import NewPolicyView from './NewPolicy.view'; + +interface IProps {} + +const NewPolicy: React.FC = () => { + const navigate = useNavigate(); + + const params = useParams<{ readonly groupId: string }>(); + + const [policyLabelState, setPolicyLabelState] = useState(null); + const [policyDescriptionState, setPolicyDescriptionState] = useState(null); + const [isPolicyLabelValidState, setIsPolicyLabelValidState] = useState(false); + const [isPolicyLabelAvailableState, setIsPolicyLabelAvaiableState] = useState(null); + const [selectedLibraryState, setSelectedLibraryState] = useState(null); + + const isSubmitEnabled = useMemo(() => { + return isPolicyLabelValidState && selectedLibraryState !== null; + }, [isPolicyLabelValidState, selectedLibraryState]); + + const onPolicyLabelChange = (value: string) => setPolicyLabelState(() => value); + const onPolicyDescriptionChange = (value: string) => setPolicyDescriptionState(() => value); + const onSetPolicyLabelValid = (value: boolean) => setIsPolicyLabelValidState(() => value); + const onSetPolicyLabelAvailable = (value: boolean | null) => setIsPolicyLabelAvaiableState(() => value); + const onLibrarySelect = (library: ILibraryName | null) => setSelectedLibraryState(() => library); + + const onCreatePolicy = (e: FormEvent) => { + e.preventDefault(); + + backendApi + .post(`/user/inline-policies/${params.groupId}`, { + label: policyLabelState, + description: policyDescriptionState, + library: selectedLibraryState, + }) + .then(() => { + navigate('/'); + }); + }; + + return ( + + ); +}; + +NewPolicy.displayName = 'NewPolicy'; +NewPolicy.defaultProps = {}; + +export default React.memo(NewPolicy); diff --git a/apps/frontend/src/components/containers/NewPolicy/NewPolicy.view.tsx b/apps/frontend/src/components/containers/NewPolicy/NewPolicy.view.tsx new file mode 100644 index 000000000..b037dd8ee --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/NewPolicy.view.tsx @@ -0,0 +1,68 @@ +import React, { type FormEvent } from 'react'; +import { useTranslation } from 'react-i18next'; + +import Nav from '@/layout/Nav'; +import EDAcceptButton from '@/ui/EDAcceptButton'; +import type { ILibraryName } from '@/interfaces/libraries'; + +import Details from './Details'; +import LibrarySelection from './LibrarySelection'; + +import classes from './NewPolicy.module.scss'; + +interface IProps { + readonly policyLabel: string | null; + readonly policyDescription: string | null; + readonly isPolicyLabelValid: boolean; + readonly isPolicyLabelAvailable: boolean | null; + readonly isSubmitEnabled: boolean; + readonly selectedLibrary: ILibraryName | null; + readonly onPolicyLabelChange: (value: string) => void; + readonly onPolicyDescriptionChange: (value: string) => void; + readonly onSetPolicyLabelValid: (value: boolean) => void; + readonly onSetPolicyLabelAvailable: (value: boolean | null) => void; + readonly onLibrarySelect: (library: ILibraryName | null) => void; + readonly onCreatePolicy: (e: FormEvent) => void; +} + +const NewPolicyView: React.FC = (props: React.PropsWithChildren) => { + const { t } = useTranslation(); + + return ( +
+
+ ); +}; + +NewPolicyView.displayName = 'NewPolicyView'; +NewPolicyView.defaultProps = {}; + +export default React.memo(NewPolicyView); diff --git a/apps/frontend/src/components/containers/NewPolicy/index.ts b/apps/frontend/src/components/containers/NewPolicy/index.ts new file mode 100644 index 000000000..3b1e2fce0 --- /dev/null +++ b/apps/frontend/src/components/containers/NewPolicy/index.ts @@ -0,0 +1,3 @@ +import NewPolicy from './NewPolicy'; + +export default NewPolicy; diff --git a/apps/frontend/src/components/containers/NotFound/NotFound.view.tsx b/apps/frontend/src/components/containers/NotFound/NotFound.view.tsx index 43ae589c3..7a1106bb2 100644 --- a/apps/frontend/src/components/containers/NotFound/NotFound.view.tsx +++ b/apps/frontend/src/components/containers/NotFound/NotFound.view.tsx @@ -17,7 +17,9 @@ const NotFoundView: React.FC = () => { return ( <>
- Exlint + + Exlint + diff --git a/apps/frontend/src/components/layout/Nav/Nav.module.scss b/apps/frontend/src/components/layout/Nav/Nav.module.scss index ffe2a44ce..b4c32d215 100644 --- a/apps/frontend/src/components/layout/Nav/Nav.module.scss +++ b/apps/frontend/src/components/layout/Nav/Nav.module.scss @@ -6,6 +6,7 @@ display: flex; justify-content: space-between; height: 80px; + min-height: 80px; padding-inline: map.get($sizes, spacing-xxl); background-image: linear-gradient( 185deg, diff --git a/apps/frontend/src/components/ui/EDInputField/EDInputField.module.scss b/apps/frontend/src/components/ui/EDInputField/EDInputField.module.scss index 355f08861..71cd8c7ad 100644 --- a/apps/frontend/src/components/ui/EDInputField/EDInputField.module.scss +++ b/apps/frontend/src/components/ui/EDInputField/EDInputField.module.scss @@ -20,3 +20,37 @@ box-shadow: inset 0 0 0 2px map.get($colors, greys-silver-sand); } } + +.inputIconContainer { + display: flex; + align-items: center; + padding-inline: map.get($sizes, spacing-l); + background-color: map.get($colors, whites-ghost-white); + border-radius: 6px; + box-shadow: inset 0 0 0 2px map.get($colors, greys-platinum); + transition: 0.3s ease-in box-shadow; + + &__icon { + width: auto; + height: 15.4px; + margin-inline-end: map.get($sizes, spacing-m); + fill: map.get($colors, greys-independence-grey); + } + + &__input { + width: 100%; + padding-block: map.get($sizes, spacing-m); + font-size: 1.5rem; + color: map.get($colors, greys-philippine-gray); + background-color: transparent; + + &::placeholder { + opacity: 1; + } + } + + &:hover, + &:focus { + box-shadow: inset 0 0 0 2px map.get($colors, greys-silver-sand); + } +} diff --git a/apps/frontend/src/components/ui/EDInputField/EDInputField.tsx b/apps/frontend/src/components/ui/EDInputField/EDInputField.tsx index 5f5a32c63..15e5f0166 100644 --- a/apps/frontend/src/components/ui/EDInputField/EDInputField.tsx +++ b/apps/frontend/src/components/ui/EDInputField/EDInputField.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import type icons from '@/assets/icons'; + import EDInputFieldView from './EDInputField.view'; interface IProps { @@ -8,6 +10,7 @@ interface IProps { readonly value: string | null; readonly maxLength?: number; readonly placeholder?: string; + readonly iconName?: keyof typeof icons; readonly onChange: (value: string) => void; } @@ -19,6 +22,7 @@ const EDInputField: React.FC = (props: React.PropsWithChildren) value={props.value} maxLength={props.maxLength} placeholder={props.placeholder} + iconName={props.iconName} onChange={props.onChange} /> ); diff --git a/apps/frontend/src/components/ui/EDInputField/EDInputField.view.tsx b/apps/frontend/src/components/ui/EDInputField/EDInputField.view.tsx index f0f33d239..7838784bc 100644 --- a/apps/frontend/src/components/ui/EDInputField/EDInputField.view.tsx +++ b/apps/frontend/src/components/ui/EDInputField/EDInputField.view.tsx @@ -1,6 +1,9 @@ import React from 'react'; import { concatDiverseClasses } from '@/utils/component'; +import type icons from '@/assets/icons'; + +import EDSvg from '../EDSvg'; import classes from './EDInputField.module.scss'; @@ -10,20 +13,38 @@ interface IProps { readonly value: string | null; readonly maxLength?: number; readonly placeholder?: string; + readonly iconName?: keyof typeof icons; readonly onChange: (value: string) => void; } const EDInputFieldView: React.FC = (props: React.PropsWithChildren) => { + if (!props.iconName) { + return ( + props.onChange(value)} + /> + ); + } + return ( - props.onChange(value)} - /> +
+ + + props.onChange(value)} + /> +
); }; diff --git a/apps/frontend/src/components/ui/EDInputFieldAvailability/EDInputFieldAvailability.module.scss b/apps/frontend/src/components/ui/EDInputFieldAvailability/EDInputFieldAvailability.module.scss index e3424c294..77eb86840 100644 --- a/apps/frontend/src/components/ui/EDInputFieldAvailability/EDInputFieldAvailability.module.scss +++ b/apps/frontend/src/components/ui/EDInputFieldAvailability/EDInputFieldAvailability.module.scss @@ -38,6 +38,7 @@ &__text { font-size: 1.5rem; color: map.get($colors, greys-onyx); + white-space: nowrap; } } } diff --git a/apps/frontend/src/components/ui/EDSelect/EDSelect.module.scss b/apps/frontend/src/components/ui/EDSelect/EDSelect.module.scss new file mode 100644 index 000000000..374d8f0f4 --- /dev/null +++ b/apps/frontend/src/components/ui/EDSelect/EDSelect.module.scss @@ -0,0 +1,67 @@ +@use 'sass:map'; + +@import '../../../styles/variables.scss'; + +.container { + position: relative; + width: 200px; + + .selectedOptionContainer { + display: flex; + align-items: center; + justify-content: space-between; + padding: map.get($sizes, spacing-m) map.get($sizes, spacing-l); + background-color: map.get($colors, whites-white); + border: 2px solid map.get($colors, greys-platinum); + border-radius: 6px; + + &--open { + border-block-end-color: transparent; + border-end-start-radius: 0; + border-end-end-radius: 0; + } + + .selectedOptionText { + display: flex; + + &__prefix { + font-size: 1.5rem; + color: map.get($colors, greys-independence-grey); + } + + &__selectedLabel { + font-size: 1.5rem; + font-weight: 500; + color: map.get($colors, greys-onyx); + } + } + + &__icon { + width: 8px; + height: auto; + fill: transparent; + stroke: map.get($colors, greys-onyx); + } + } + + .optionsContainer { + position: absolute; + display: flex; + flex-direction: column; + width: 100%; + padding-inline: map.get($sizes, spacing-l); + background-color: map.get($colors, whites-white); + border: 2px solid map.get($colors, greys-platinum); + + &__option { + padding-block: map.get($sizes, spacing-m); + font-size: 1.5rem; + color: map.get($colors, greys-independence-grey); + cursor: default; + + &:not(:last-child) { + border-bottom: 2px solid map.get($colors, greys-platinum); + } + } + } +} diff --git a/apps/frontend/src/components/ui/EDSelect/EDSelect.tsx b/apps/frontend/src/components/ui/EDSelect/EDSelect.tsx new file mode 100644 index 000000000..35816ef06 --- /dev/null +++ b/apps/frontend/src/components/ui/EDSelect/EDSelect.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +import { useClickOutside } from '@/hooks/click-outside'; + +import type { IOption } from './interfaces/option'; + +import EDSelectView from './EDSelect.view'; + +interface IProps { + readonly className?: string; + readonly options: IOption[]; + readonly selectedIndex: number; + readonly prefix: string; + readonly onSelect: (index: number) => void; +} + +const EDSelect = (props: React.PropsWithChildren>) => { + const { + ref: selectRef, + isVisible: isSelectVisible, + toggleVisibility: toggleSelectVisibility, + } = useClickOutside(false); + + const optionsLabels = props.options.map((option) => option.label); + + return ( + + ); +}; + +EDSelect.displayName = 'EDSelect'; +EDSelect.defaultProps = {}; + +export default React.memo(EDSelect); diff --git a/apps/frontend/src/components/ui/EDSelect/EDSelect.view.tsx b/apps/frontend/src/components/ui/EDSelect/EDSelect.view.tsx new file mode 100644 index 000000000..452d770f7 --- /dev/null +++ b/apps/frontend/src/components/ui/EDSelect/EDSelect.view.tsx @@ -0,0 +1,68 @@ +import React, { type RefObject } from 'react'; +import { Trans } from 'react-i18next'; + +import { concatClasses, concatDiverseClasses } from '@/utils/component'; + +import EDSvg from '../EDSvg'; + +import classes from './EDSelect.module.scss'; + +interface IProps { + readonly className?: string; + readonly selectRef: RefObject; + readonly isSelectVisible: boolean; + readonly options: string[]; + readonly selectedIndex: number; + readonly prefix: string; + readonly onSelect: (index: number) => void; + readonly toggleSelectVisibility: VoidFunction; +} + +const EDSelectView: React.FC = (props: React.PropsWithChildren) => { + const selectedOption = props.options[props.selectedIndex]!; + + const selectedOptionClasses = concatClasses( + classes, + 'selectedOptionContainer', + props.isSelectVisible ? 'selectedOptionContainer--open' : null, + ); + + return ( +
+
+
+ + {props.prefix} + : +   + + + {selectedOption} +
+ +
+ + {props.isSelectVisible && ( +
+ {props.options.map((option, index) => ( + props.onSelect(index)} + > + {option} + + ))} +
+ )} +
+ ); +}; + +EDSelectView.displayName = 'EDSelectView'; +EDSelectView.defaultProps = {}; + +export default React.memo(EDSelectView); diff --git a/apps/frontend/src/components/ui/EDSelect/index.ts b/apps/frontend/src/components/ui/EDSelect/index.ts new file mode 100644 index 000000000..b8976c12a --- /dev/null +++ b/apps/frontend/src/components/ui/EDSelect/index.ts @@ -0,0 +1,3 @@ +import EDSelect from './EDSelect'; + +export default EDSelect; diff --git a/apps/frontend/src/components/ui/EDSelect/interfaces/option.ts b/apps/frontend/src/components/ui/EDSelect/interfaces/option.ts new file mode 100644 index 000000000..489927c26 --- /dev/null +++ b/apps/frontend/src/components/ui/EDSelect/interfaces/option.ts @@ -0,0 +1,4 @@ +export interface IOption { + readonly value: T; + readonly label: string; +} diff --git a/apps/frontend/src/components/ui/EDSvg/EDSvg.view.tsx b/apps/frontend/src/components/ui/EDSvg/EDSvg.view.tsx index 85bda8769..d14b7ef21 100644 --- a/apps/frontend/src/components/ui/EDSvg/EDSvg.view.tsx +++ b/apps/frontend/src/components/ui/EDSvg/EDSvg.view.tsx @@ -17,6 +17,7 @@ const EDSvgView: React.FC = (props: React.PropsWithChildren) => const clickHandler = (e: React.MouseEvent) => { e.preventDefault(); + e.stopPropagation(); props.onClick!(); }; diff --git a/apps/frontend/src/data/depcheck-data.ts b/apps/frontend/src/data/depcheck-data.ts deleted file mode 100644 index df3200bec..000000000 --- a/apps/frontend/src/data/depcheck-data.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { LibraryCategory } from '../models/library-category'; -import { LibraryType } from '../models/library-type'; - -export const depcheckData = { - name: 'Depcheck', - author: 'Djordje Lukic, Junle Li', - description: 'Check your npm module for unused dependencies.', - type: [LibraryType.Linters], - category: [LibraryCategory.Dependencies], -}; diff --git a/apps/frontend/src/data/inflint-data.ts b/apps/frontend/src/data/inflint-data.ts deleted file mode 100644 index d0cb7c640..000000000 --- a/apps/frontend/src/data/inflint-data.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { LibraryCategory } from '../models/library-category'; -import { LibraryType } from '../models/library-type'; - -export const inflintData = { - name: 'Inflint', - author: 'Tal Rofe', - description: 'Inflint is a tool which scans and verifies file name conventions.', - type: [LibraryType.Linters], - category: [LibraryCategory.FileSystem], -}; diff --git a/apps/frontend/src/i18n/en.ts b/apps/frontend/src/i18n/en.ts index e7924eb97..ac1d9cd3b 100644 --- a/apps/frontend/src/i18n/en.ts +++ b/apps/frontend/src/i18n/en.ts @@ -187,6 +187,36 @@ const en = { items: 'items', pages: 'pages', }, + newPolicy: { + formHeader: 'Create a new policy', + formSubHeader: + 'A policy contains a custom-made rule set and configurations, including CLI usage history.', + group: 'Group', + policyLabel: 'Policy label', + descriptionLabel: 'Description', + descriptionLabelPostfix: '(optional)', + librarySelection: { + header: 'Choose a Library', + filterInputPlaceholder: 'Search Library', + selectPrefix: 'Sort', + languageSort: 'Language', + filters: { + all: 'All', + languageTitle: 'Language', + typeFilter: 'Types', + categoryFilter: 'Categories', + agnostic: 'Agnostic', + linters: 'Linters', + formatters: 'Formatters', + code: 'Code', + fileSystem: 'File System', + styles: 'Styles', + dependencies: 'Dependecies', + }, + selected: 'Selected', + }, + formSubmit: 'Create Policy', + }, }; export default en; diff --git a/apps/frontend/src/interfaces/libraries.ts b/apps/frontend/src/interfaces/libraries.ts index 92a985793..e53da42aa 100644 --- a/apps/frontend/src/interfaces/libraries.ts +++ b/apps/frontend/src/interfaces/libraries.ts @@ -1,28 +1 @@ -import type { LibraryCategory } from '../models/library-category'; -import type { LibraryType } from '../models/library-type'; - -interface ILibraryRule { - readonly description: string; - readonly configApi: string; - readonly hasAutoFix?: boolean; - readonly category?: string; -} - -export interface ILibraryData { - readonly name: string; - readonly author: string; - readonly description: string; - readonly type: LibraryType[]; - readonly category: LibraryCategory[]; - readonly rules?: Record; -} - -export interface ILbirariesData { - readonly eslint: ILibraryData; - readonly depcheck: ILibraryData; - readonly inflint: ILibraryData; - readonly prettier: ILibraryData; - readonly stylelint: ILibraryData; -} - export type ILibraryName = 'ESLint' | 'Depcheck' | 'Inflint' | 'Prettier' | 'Stylelint'; diff --git a/apps/frontend/src/interfaces/responses.ts b/apps/frontend/src/interfaces/responses.ts index 9cdbc0450..158da8463 100644 --- a/apps/frontend/src/interfaces/responses.ts +++ b/apps/frontend/src/interfaces/responses.ts @@ -5,11 +5,15 @@ export interface IAutoAuthResponseData { readonly createdAt: number; } -export interface IRefreshTokenResponseData { - readonly accessToken: string; -} - export interface ICliAuthResponseData { readonly cliToken: string; readonly email: string; } + +export interface IGetGroupResponse { + readonly label: string; +} + +export interface IAvailableLabelResponse { + readonly isAvailable: boolean; +} diff --git a/apps/frontend/src/models/library-category.ts b/apps/frontend/src/models/library-category.ts deleted file mode 100644 index 3a4652568..000000000 --- a/apps/frontend/src/models/library-category.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum LibraryCategory { - Code, - FileSystem, - Styles, - Dependencies, -} diff --git a/apps/frontend/src/models/library-type.ts b/apps/frontend/src/models/library-type.ts deleted file mode 100644 index cbd4a24c9..000000000 --- a/apps/frontend/src/models/library-type.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum LibraryType { - Linters, - Formatters, -} diff --git a/apps/frontend/src/pages/NewPolicy.tsx b/apps/frontend/src/pages/NewPolicy.tsx new file mode 100644 index 000000000..49fc1907a --- /dev/null +++ b/apps/frontend/src/pages/NewPolicy.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import NewPolicy from '@/containers/NewPolicy'; + +interface IProps {} + +const NewPolicyPage: React.FC = () => { + return ; +}; + +NewPolicyPage.displayName = 'NewPolicyPage'; +NewPolicyPage.defaultProps = {}; + +export default NewPolicyPage; diff --git a/apps/frontend/src/store/interfaces/responses.ts b/apps/frontend/src/store/interfaces/responses.ts new file mode 100644 index 000000000..82c067b5d --- /dev/null +++ b/apps/frontend/src/store/interfaces/responses.ts @@ -0,0 +1,3 @@ +export interface IRefreshTokenResponseData { + readonly accessToken: string; +} diff --git a/apps/frontend/src/store/middlewares/auth.ts b/apps/frontend/src/store/middlewares/auth.ts index 687d253da..fc6a2963e 100644 --- a/apps/frontend/src/store/middlewares/auth.ts +++ b/apps/frontend/src/store/middlewares/auth.ts @@ -6,8 +6,8 @@ import { } from '@reduxjs/toolkit'; import { backendApi } from '@/utils/http'; -import type { IRefreshTokenResponseData } from '@/interfaces/responses'; +import type { IRefreshTokenResponseData } from '../interfaces/responses'; import { authActions } from '../reducers/auth'; import { ACCESS_TOKEN_REFRESH_TIMEOUT } from '../models/auth'; diff --git a/apps/frontend/src/styles/custom.scss b/apps/frontend/src/styles/custom.scss index 8710ed6d9..a89366122 100644 --- a/apps/frontend/src/styles/custom.scss +++ b/apps/frontend/src/styles/custom.scss @@ -57,23 +57,12 @@ p { margin: 0; } -h1 { - margin: 0; -} - -h2 { - margin: 0; -} - -h3 { - margin: 0; -} - -h4 { - margin: 0; -} - -h5 { +h1, +h2, +h3, +h4, +h5, +h6 { margin: 0; } diff --git a/apps/frontend/src/styles/variables.scss b/apps/frontend/src/styles/variables.scss index e7a2ce34e..fecf4d12b 100644 --- a/apps/frontend/src/styles/variables.scss +++ b/apps/frontend/src/styles/variables.scss @@ -7,7 +7,7 @@ $sizes: ( spacing-l: 16px, spacing-xl: 24px, spacing-xxl: 32px, - spacing-xxl-2: 52px, + spacing-xxl-2: 56px, spacing-xxl-3: 64px, spacing-xxl-4: 128px, ); diff --git a/apps/frontend/stylelint.config.cjs b/apps/frontend/stylelint.config.cjs index 34973360e..bb7e4079b 100644 --- a/apps/frontend/stylelint.config.cjs +++ b/apps/frontend/stylelint.config.cjs @@ -26,8 +26,8 @@ module.exports = { 'scss/at-import-partial-extension': null, 'scale-unlimited/declaration-strict-value': [ - ['/color/', '/padding/', 'top', 'bottom', '/margin/', 'font-size', 'fill'], - { ignoreVariables: false, ignoreValues: ['transparent', '/rem/', '0'] }, + ['/color/', '/padding/', 'top', 'bottom', '/margin/', 'font-size', 'fill', '/gap/'], + { ignoreVariables: false, ignoreValues: ['transparent', '/rem/', '0', 'auto'] }, ], }, }; diff --git a/docker/Dockerfile.backend-dev b/docker/Dockerfile.backend-dev index f863ede55..37fdbdea8 100644 --- a/docker/Dockerfile.backend-dev +++ b/docker/Dockerfile.backend-dev @@ -8,9 +8,9 @@ COPY ./package.json ./pnpm-workspace.yaml ./.npmrc ./ COPY ./apps/backend/package.json ./apps/backend/ COPY ./prisma/schema.prisma ./prisma/ -RUN pnpm add -D @prisma/client -w RUN pnpm --filter backend i -COPY ./ ./ +COPY ./tsconfig.base.json ./ +COPY ./apps/backend/ ./apps/backend/ CMD ["pnpm", "--filter", "backend", "start:dev:docker"] diff --git a/docker/Dockerfile.cli-backend-dev b/docker/Dockerfile.cli-backend-dev index 5333697db..9aea81a92 100644 --- a/docker/Dockerfile.cli-backend-dev +++ b/docker/Dockerfile.cli-backend-dev @@ -8,9 +8,9 @@ COPY ./package.json ./pnpm-workspace.yaml ./.npmrc ./ COPY ./apps/cli-backend/package.json ./apps/cli-backend/ COPY ./prisma/schema.prisma ./prisma/ -RUN pnpm add -D @prisma/client -w RUN pnpm --filter cli-backend i -COPY ./ ./ +COPY ./tsconfig.base.json ./ +COPY ./apps/cli-backend/ ./apps/cli-backend/ CMD ["pnpm", "--filter", "cli-backend", "start:dev:docker"] diff --git a/docker/Dockerfile.frontend-dev b/docker/Dockerfile.frontend-dev index 8c5724245..58bd36510 100644 --- a/docker/Dockerfile.frontend-dev +++ b/docker/Dockerfile.frontend-dev @@ -9,6 +9,7 @@ COPY ./apps/frontend/package.json ./apps/frontend/ RUN pnpm --filter frontend i -COPY ./ ./ +COPY ./tsconfig.base.json ./ +COPY ./apps/frontend/ ./apps/frontend/ CMD ["pnpm", "--filter", "frontend", "start:dev:docker"] diff --git a/docker/scripts/restart-cluster.sh b/docker/scripts/restart-cluster.sh index 31db02fbb..b79e954ae 100755 --- a/docker/scripts/restart-cluster.sh +++ b/docker/scripts/restart-cluster.sh @@ -1,3 +1,5 @@ #!/bin/bash -docker-compose restart \ No newline at end of file +docker-compose restart + +node ./docker/scripts/open-dashboard.js \ No newline at end of file diff --git a/package.json b/package.json index 95e36ddde..34d5e5678 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "devDependencies": { "@commitlint/cli": "17.1.2", "@exlint.io/inflint": "1.2.9", - "@prisma/client": "4.3.1", "@types/node": "17.0.35", "@typescript-eslint/eslint-plugin": "5.38.0", "@typescript-eslint/parser": "5.38.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce50eba96..fd5c14a44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,6 @@ importers: specifiers: '@commitlint/cli': 17.1.2 '@exlint.io/inflint': 1.2.9 - '@prisma/client': 4.3.1 '@types/node': 17.0.35 '@typescript-eslint/eslint-plugin': 5.38.0 '@typescript-eslint/parser': 5.38.0 @@ -37,7 +36,6 @@ importers: devDependencies: '@commitlint/cli': 17.1.2 '@exlint.io/inflint': 1.2.9_2czvso7fgvx5jwiqwq3hb3lwcu - '@prisma/client': 4.3.1_prisma@4.3.1 '@types/node': 17.0.35 '@typescript-eslint/eslint-plugin': 5.38.0_wsb62dxj2oqwgas4kadjymcmry '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha @@ -3262,9 +3260,11 @@ packages: dependencies: '@prisma/engines-version': 4.3.0-32.c875e43600dfe042452e0b868f7a48b817b9640b prisma: 4.3.1 + dev: false /@prisma/engines-version/4.3.0-32.c875e43600dfe042452e0b868f7a48b817b9640b: resolution: {integrity: sha512-8yWpXkQRmiSfsi2Wb/ZS5D3RFbeu/btL9Pm/gdF4phB0Lo5KGsDFMxFMgaD64mwED2nHc8ZaEJg/+4Jymb9Znw==} + dev: false /@prisma/engines/4.3.1: resolution: {integrity: sha512-4JF/uMaEDAPdcdZNOrnzE3BvrbGpjgV0FcPT3EVoi6I86fWkloqqxBt+KcK/+fIRR0Pxj66uGR9wVH8U1Y13JA==} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0e2112ca3..2cc4663ed 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -71,6 +71,7 @@ model InlinePolicy { groupId String @db.ObjectId label String + description String? library PolicyLibrary configuration Json? rules Json?