diff --git a/frontend/cypress/integration/projects/overview.spec.ts b/frontend/cypress/integration/projects/overview.spec.ts index 126542288f..1435e85e02 100644 --- a/frontend/cypress/integration/projects/overview.spec.ts +++ b/frontend/cypress/integration/projects/overview.spec.ts @@ -23,12 +23,12 @@ describe('project overview', () => { after(() => { cy.request({ method: 'DELETE', - url: `${baseUrl}/api/admin/features/${featureToggleName}-A`, + url: `${baseUrl}/api/admin/projects/default/features/${featureToggleName}-A`, failOnStatusCode: false, }); cy.request({ method: 'DELETE', - url: `${baseUrl}/api/admin/features/${featureToggleName}-B`, + url: `${baseUrl}/api/admin/projects/default/features/${featureToggleName}-B`, failOnStatusCode: false, }); cy.request({ diff --git a/frontend/cypress/support/API.ts b/frontend/cypress/support/API.ts index cde08909f7..d0dde3d2f4 100644 --- a/frontend/cypress/support/API.ts +++ b/frontend/cypress/support/API.ts @@ -24,7 +24,7 @@ export const createFeature_API = ( export const deleteFeature_API = (name: string): Chainable => { cy.request({ method: 'DELETE', - url: `${baseUrl}/api/admin/features/${name}`, + url: `${baseUrl}/api/admin/projects/default/features/${name}`, }); return cy.request({ method: 'DELETE', diff --git a/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx b/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx index 5ecd5029ab..6aede2799f 100644 --- a/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx +++ b/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx @@ -95,24 +95,18 @@ export const ArchiveTable = ({ const columns = useMemo( () => [ - ...(uiConfig?.flags?.bulkOperations - ? [ - { - id: 'Select', - Header: ({ getToggleAllRowsSelectedProps }: any) => ( - - ), - Cell: ({ row }: any) => ( - - ), - maxWidth: 50, - disableSortBy: true, - hideInMenu: true, - }, - ] - : []), + { + id: 'Select', + Header: ({ getToggleAllRowsSelectedProps }: any) => ( + + ), + Cell: ({ row }: any) => ( + + ), + maxWidth: 50, + disableSortBy: true, + hideInMenu: true, + }, { Header: 'Seen', width: 85, diff --git a/frontend/src/component/archive/ArchiveTable/ArchivedFeatureActionCell/ArchivedFeatureDeleteConfirm/ArchivedFeatureDeleteConfirm.tsx b/frontend/src/component/archive/ArchiveTable/ArchivedFeatureActionCell/ArchivedFeatureDeleteConfirm/ArchivedFeatureDeleteConfirm.tsx index b46dad7960..8fb50babe2 100644 --- a/frontend/src/component/archive/ArchiveTable/ArchivedFeatureActionCell/ArchivedFeatureDeleteConfirm/ArchivedFeatureDeleteConfirm.tsx +++ b/frontend/src/component/archive/ArchiveTable/ArchivedFeatureActionCell/ArchivedFeatureDeleteConfirm/ArchivedFeatureDeleteConfirm.tsx @@ -43,12 +43,7 @@ export const ArchivedFeatureDeleteConfirm = ({ if (deletedFeatures.length === 0) { return; } - - if (uiConfig?.flags?.bulkOperations) { - await deleteFeatures(projectId, deletedFeatures); - } else { - await deleteFeature(deletedFeatures[0]); - } + await deleteFeatures(projectId, deletedFeatures); await refetch(); setToastData({ diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx index 6d50b1bcb5..4772dc117c 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -240,24 +240,18 @@ export const ProjectFeatureToggles = ({ const columns = useMemo( () => [ - ...(uiConfig?.flags?.bulkOperations - ? [ - { - id: 'Select', - Header: ({ getToggleAllRowsSelectedProps }: any) => ( - - ), - Cell: ({ row }: any) => ( - - ), - maxWidth: 50, - disableSortBy: true, - hideInMenu: true, - }, - ] - : []), + { + id: 'Select', + Header: ({ getToggleAllRowsSelectedProps }: any) => ( + + ), + Cell: ({ row }: any) => ( + + ), + maxWidth: 50, + disableSortBy: true, + hideInMenu: true, + }, { id: 'favorite', Header: ( diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index 8a6a765229..c94a63ccb8 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -45,7 +45,6 @@ export interface IFlags { crOnVariants?: boolean; proPlanAutoCharge?: boolean; notifications?: boolean; - bulkOperations?: boolean; personalAccessTokensKillSwitch?: boolean; demo?: boolean; strategyTitle?: boolean; diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 374f6f5dc2..f5a607d20e 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -67,7 +67,6 @@ exports[`should create default config 1`] = ` }, "flags": { "anonymiseEventLog": false, - "bulkOperations": false, "caseInsensitiveInOperators": false, "cleanClientApi": false, "crOnVariants": false, @@ -94,7 +93,6 @@ exports[`should create default config 1`] = ` "flagResolver": FlagResolver { "experiments": { "anonymiseEventLog": false, - "bulkOperations": false, "caseInsensitiveInOperators": false, "cleanClientApi": false, "crOnVariants": false, diff --git a/src/lib/error/api-error.test.ts b/src/lib/error/api-error.test.ts index 8077246fd8..ed52fce940 100644 --- a/src/lib/error/api-error.test.ts +++ b/src/lib/error/api-error.test.ts @@ -1,3 +1,4 @@ +import owasp from 'owasp-password-strength-test'; import { ErrorObject } from 'ajv'; import { ApiErrorSchema, @@ -9,6 +10,7 @@ import { UnleashError, } from './api-error'; import BadDataError from './bad-data-error'; +import OwaspValidationError from './owasp-validation-error'; describe('v5 deprecation: backwards compatibility', () => { it.each(UnleashApiErrorTypes)( @@ -320,3 +322,14 @@ describe('OpenAPI error conversion', () => { expect(description.includes(illegalValue)).toBeTruthy(); }); }); + +describe('Error serialization special cases', () => { + it('OwaspValidationErrors: adds `validationErrors` to `details`', () => { + const results = owasp.test('123'); + const error = new OwaspValidationError(results); + const json = fromLegacyError(error).toJSON(); + + expect(json.details!![0].message).toBe(results.errors[0]); + expect(json.details!![0].validationErrors).toBe(results.errors); + }); +}); diff --git a/src/lib/error/api-error.ts b/src/lib/error/api-error.ts index 70fa805118..028c1307ac 100644 --- a/src/lib/error/api-error.ts +++ b/src/lib/error/api-error.ts @@ -1,6 +1,7 @@ import { v4 as uuidV4 } from 'uuid'; import { FromSchema } from 'json-schema-to-ts'; import { ErrorObject } from 'ajv'; +import OwaspValidationError from './owasp-validation-error'; export const UnleashApiErrorTypes = [ 'ContentTypeError', @@ -14,7 +15,6 @@ export const UnleashApiErrorTypes = [ 'NotFoundError', 'NotImplementedError', 'OperationDeniedError', - 'OwaspValidationError', 'PasswordMismatch', 'PasswordMismatchError', 'PasswordUndefinedError', @@ -34,6 +34,7 @@ const UnleashApiErrorTypesWithExtraData = [ 'AuthenticationRequired', 'NoAccessError', 'InvalidTokenError', + 'OwaspValidationError', ] as const; const AllUnleashApiErrorTypes = [ @@ -136,6 +137,15 @@ type UnleashErrorData = ...ValidationErrorDescription[], ]; } + | { + name: 'OwaspValidationError'; + details: [ + { + validationErrors: string[]; + message: string; + }, + ]; + } ); export class UnleashError extends Error { @@ -245,6 +255,15 @@ export const fromLegacyError = (e: Error): UnleashError => { }); } + if (name === 'OwaspValidationError') { + return new UnleashError({ + name, + message: + 'Password validation failed. Refer to the `details` property.', + details: (e as OwaspValidationError).toJSON().details, + }); + } + if (name === 'AuthenticationRequired') { return new UnleashError({ name, diff --git a/src/lib/routes/admin-api/project/project-archive.ts b/src/lib/routes/admin-api/project/project-archive.ts index 13396a4046..02450e049a 100644 --- a/src/lib/routes/admin-api/project/project-archive.ts +++ b/src/lib/routes/admin-api/project/project-archive.ts @@ -14,7 +14,6 @@ import { IAuthRequest } from '../../unleash-types'; import { OpenApiService } from '../../../services/openapi-service'; import { emptyResponse } from '../../../openapi/util/standard-responses'; import { BatchFeaturesSchema, createRequestSchema } from '../../../openapi'; -import NotFoundError from '../../../error/notfound-error'; import Controller from '../../controller'; const PATH = '/:projectId'; @@ -105,9 +104,6 @@ export default class ProjectArchiveController extends Controller { req: IAuthRequest, res: Response, ): Promise { - if (!this.flagResolver.isEnabled('bulkOperations')) { - throw new NotFoundError('Bulk operations are not enabled'); - } const { projectId } = req.params; const { features } = req.body; const user = extractUsername(req); @@ -119,9 +115,6 @@ export default class ProjectArchiveController extends Controller { req: IAuthRequest, res: Response, ): Promise { - if (!this.flagResolver.isEnabled('bulkOperations')) { - throw new NotFoundError('Bulk operations are not enabled'); - } const { projectId } = req.params; const { features } = req.body; const user = extractUsername(req); @@ -133,10 +126,6 @@ export default class ProjectArchiveController extends Controller { req: IAuthRequest, res: Response, ): Promise { - if (!this.flagResolver.isEnabled('bulkOperations')) { - throw new NotFoundError('Bulk operations are not enabled'); - } - const { features } = req.body; const { projectId } = req.params; const userName = extractUsername(req); diff --git a/src/lib/routes/admin-api/project/project-features.ts b/src/lib/routes/admin-api/project/project-features.ts index 3326213c7f..f83d989e8c 100644 --- a/src/lib/routes/admin-api/project/project-features.ts +++ b/src/lib/routes/admin-api/project/project-features.ts @@ -40,7 +40,6 @@ import { } from '../../../openapi'; import { OpenApiService, FeatureToggleService } from '../../../services'; import { querySchema } from '../../../schema/feature-schema'; -import NotFoundError from '../../../error/notfound-error'; import { BatchStaleSchema } from '../../../openapi/spec/batch-stale-schema'; interface FeatureStrategyParams { @@ -594,10 +593,6 @@ export default class ProjectFeaturesController extends Controller { req: IAuthRequest<{ projectId: string }, void, BatchStaleSchema>, res: Response, ): Promise { - if (!this.flagResolver.isEnabled('bulkOperations')) { - throw new NotFoundError('Bulk operations are not enabled'); - } - const { features, stale } = req.body; const { projectId } = req.params; const userName = extractUsername(req); diff --git a/src/lib/routes/admin-api/tag.ts b/src/lib/routes/admin-api/tag.ts index 0d13202bf2..1f8c02b240 100644 --- a/src/lib/routes/admin-api/tag.ts +++ b/src/lib/routes/admin-api/tag.ts @@ -24,7 +24,6 @@ import { import { emptyResponse } from '../../openapi/util/standard-responses'; import FeatureTagService from 'lib/services/feature-tag-service'; import { TagsBulkAddSchema } from '../../openapi/spec/tags-bulk-add-schema'; -import NotFoundError from '../../error/notfound-error'; import { IFlagResolver } from '../../types'; const version = 1; @@ -214,9 +213,6 @@ class TagController extends Controller { req: IAuthRequest, res: Response, ): Promise { - if (!this.flagResolver.isEnabled('bulkOperations')) { - throw new NotFoundError('Bulk operations are not enabled'); - } const { features, tags } = req.body; const userName = extractUsername(req); await this.featureTagService.updateTags( diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index b0d40a7944..61724b233a 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -49,10 +49,6 @@ const flags = { process.env.UNLEASH_PRO_PLAN_AUTO_CHARGE, false, ), - bulkOperations: parseEnvVarBoolean( - process.env.UNLEASH_BULK_OPERATIONS, - false, - ), personalAccessTokensKillSwitch: parseEnvVarBoolean( process.env.UNLEASH_PAT_KILL_SWITCH, false, diff --git a/src/server-dev.ts b/src/server-dev.ts index 4b782ba940..f2d0ba5c1e 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -39,7 +39,6 @@ process.nextTick(async () => { anonymiseEventLog: false, responseTimeWithAppNameKillSwitch: false, newProjectOverview: true, - bulkOperations: true, optimal304: true, optimal304Differ: false, }, diff --git a/src/test/e2e/api/admin/feature-archive.e2e.test.ts b/src/test/e2e/api/admin/feature-archive.e2e.test.ts index a373be066e..9fecb9c16b 100644 --- a/src/test/e2e/api/admin/feature-archive.e2e.test.ts +++ b/src/test/e2e/api/admin/feature-archive.e2e.test.ts @@ -15,7 +15,6 @@ beforeAll(async () => { experimental: { flags: { strictSchemaValidation: true, - bulkOperations: true, }, }, }); diff --git a/src/test/e2e/api/admin/project/features.e2e.test.ts b/src/test/e2e/api/admin/project/features.e2e.test.ts index c249bb27b0..b80f1c8baf 100644 --- a/src/test/e2e/api/admin/project/features.e2e.test.ts +++ b/src/test/e2e/api/admin/project/features.e2e.test.ts @@ -89,7 +89,6 @@ beforeAll(async () => { experimental: { flags: { strictSchemaValidation: true, - bulkOperations: true, }, }, }, diff --git a/src/test/e2e/api/admin/tags.e2e.test.ts b/src/test/e2e/api/admin/tags.e2e.test.ts index c8e9ef5456..1cd26fd98f 100644 --- a/src/test/e2e/api/admin/tags.e2e.test.ts +++ b/src/test/e2e/api/admin/tags.e2e.test.ts @@ -11,7 +11,6 @@ beforeAll(async () => { experimental: { flags: { strictSchemaValidation: true, - bulkOperations: true, }, }, });