diff --git a/packages/api-clients/open-api/bffApi.yml b/packages/api-clients/open-api/bffApi.yml index b796338434..9903c90b43 100644 --- a/packages/api-clients/open-api/bffApi.yml +++ b/packages/api-clients/open-api/bffApi.yml @@ -2116,7 +2116,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/CreatedResource" + $ref: "#/components/schemas/CreatedEServiceDescriptor" "400": description: Invalid input headers: @@ -2398,13 +2398,6 @@ paths: schema: type: string format: uuid - requestBody: - description: An E-Service Descriptor seed - content: - application/json: - schema: - $ref: "#/components/schemas/EServiceDescriptorSeed" - required: true responses: "200": description: EService Descriptor created. @@ -3880,6 +3873,227 @@ paths: application/json: schema: $ref: "#/components/schemas/Problem" + /export/eservices/{eserviceId}/descriptors/{descriptorId}: + parameters: + - $ref: "#/components/parameters/CorrelationIdHeader" + - in: path + name: eserviceId + required: true + schema: + type: string + format: uuid + - in: path + name: descriptorId + required: true + schema: + type: string + format: uuid + get: + tags: + - eservices + summary: Export EService descriptor + operationId: exportEServiceDescriptor + responses: + "200": + description: EService descriptor exported + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + content: + application/json: + schema: + $ref: "#/components/schemas/FileResource" + "400": + description: Invalid input + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + "404": + description: Not found + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + /import/eservices/presignedUrl: + parameters: + - $ref: "#/components/parameters/CorrelationIdHeader" + - in: query + name: fileName + required: true + schema: + type: string + get: + tags: + - eservices + summary: Get presigned URL + operationId: getImportEservicePresignedUrl + responses: + "200": + description: Presigned URL retrieved + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + content: + application/json: + schema: + $ref: "#/components/schemas/PresignedUrl" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + /import/eservices: + parameters: + - $ref: "#/components/parameters/CorrelationIdHeader" + post: + tags: + - eservices + summary: Import EService + operationId: importEService + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/FileResource" + responses: + "200": + description: EServices imported + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + content: + application/json: + schema: + $ref: "#/components/schemas/CreatedEServiceDescriptor" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + "409": + description: Conflict + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available "/producers": parameters: - $ref: "#/components/parameters/CorrelationIdHeader" @@ -9807,87 +10021,6 @@ paths: schema: type: integer description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available - "/clients/{clientId}/users/{userId}/keys": - parameters: - - $ref: "#/components/parameters/CorrelationIdHeader" - - name: clientId - in: path - description: ID of the client holding the key - required: true - schema: - type: string - format: uuid - - name: userId - in: path - required: true - description: ID of the User that the added keys MUST belong to - schema: - type: string - format: uuid - get: - tags: - - clients - summary: Returns a set of keys by user ID and client ID. - description: "Given a user and a client it returns its corresponding set of keys, if any" - operationId: getClientUserKeys - responses: - "200": - description: returns the corresponding array of keys - content: - application/json: - schema: - $ref: "#/components/schemas/PublicKeys" - headers: - "X-Rate-Limit-Limit": - schema: - type: integer - description: Max allowed requests within time interval - "X-Rate-Limit-Remaining": - schema: - type: integer - description: Remaining requests within time interval - "X-Rate-Limit-Interval": - schema: - type: integer - description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available - "401": - description: Unauthorized - content: - application/json: - schema: - $ref: "#/components/schemas/Problem" - headers: - "X-Rate-Limit-Limit": - schema: - type: integer - description: Max allowed requests within time interval - "X-Rate-Limit-Remaining": - schema: - type: integer - description: Remaining requests within time interval - "X-Rate-Limit-Interval": - schema: - type: integer - description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available - "404": - description: Client id not found - content: - application/json: - schema: - $ref: "#/components/schemas/Problem" - headers: - "X-Rate-Limit-Limit": - schema: - type: integer - description: Max allowed requests within time interval - "X-Rate-Limit-Remaining": - schema: - type: integer - description: Remaining requests within time interval - "X-Rate-Limit-Interval": - schema: - type: integer - description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available /purposes/riskAnalysis/latest: parameters: - $ref: "#/components/parameters/CorrelationIdHeader" @@ -10642,37 +10775,6 @@ components: type: string required: - description - EServiceDescriptorSeed: - required: - - audience - - voucherLifespan - - dailyCallsPerConsumer - - dailyCallsTotal - - agreementApprovalPolicy - - attributes - type: object - properties: - description: - type: string - audience: - type: array - items: - type: string - voucherLifespan: - type: integer - format: int32 - dailyCallsPerConsumer: - description: "maximum number of daily calls that this descriptor can afford." - type: integer - format: int32 - dailyCallsTotal: - description: "total daily calls available for this e-service." - type: integer - format: int32 - agreementApprovalPolicy: - $ref: "#/components/schemas/AgreementApprovalPolicy" - attributes: - $ref: "#/components/schemas/DescriptorAttributesSeed" CatalogEServiceDescriptor: type: object required: @@ -11475,6 +11577,14 @@ components: - offset - limit - totalCount + PresignedUrl: + type: object + properties: + url: + type: string + format: uri + required: + - url ProducerEService: type: object required: @@ -11722,12 +11832,8 @@ components: properties: title: type: string - minLength: 5 - maxLength: 60 description: type: string - minLength: 10 - maxLength: 250 isFreeOfCharge: type: boolean freeOfChargeReason: @@ -11736,7 +11842,6 @@ components: description: "maximum number of daily calls that this version can perform." type: integer format: int32 - minimum: 0 required: - title - description @@ -12241,6 +12346,17 @@ components: required: - origin - value + FileResource: + type: object + properties: + filename: + type: string + url: + type: string + format: uri + required: + - url + - filename MailKind: type: string enum: diff --git a/packages/bff/src/model/api/converters/catalogClientApiConverter.ts b/packages/bff/src/model/api/converters/catalogClientApiConverter.ts index 868181165e..e2351dfd22 100644 --- a/packages/bff/src/model/api/converters/catalogClientApiConverter.ts +++ b/packages/bff/src/model/api/converters/catalogClientApiConverter.ts @@ -179,7 +179,11 @@ export function toBffCatalogApiEserviceRiskAnalysis( ) .reduce((answers: bffApi.RiskAnalysisForm["answers"], answer) => { const key = answer.key; - if (answers[key] && answer.value) { + if (!answers[key]) { + answers[key] = []; + } + + if (answer.value) { answers[key] = [...answers[key], answer.value]; } else { answers[key] = []; diff --git a/packages/bff/src/model/domain/errors.ts b/packages/bff/src/model/domain/errors.ts index 6123f20abf..ccf83f4803 100644 --- a/packages/bff/src/model/domain/errors.ts +++ b/packages/bff/src/model/domain/errors.ts @@ -20,6 +20,8 @@ export const errorCodes = { eserviceDescriptorNotFound: "0013", purposeDraftVersionNotFound: "0014", invalidRiskAnalysisContentType: "0015", + missingInterface: "0016", + eserviceRiskNotFound: "0017", }; export type ErrorCodes = keyof typeof errorCodes; @@ -158,3 +160,25 @@ export function invalidRiskAnalysisContentType( title: "Invalid Risk Analysis content type", }); } + +export function missingInterface( + eserviceId: string, + descriptorId: string +): ApiError<ErrorCodes> { + return new ApiError({ + detail: `Missing interface for Eservice ${eserviceId} and descriptor ${descriptorId}`, + code: "missingInterface", + title: "Missing interface", + }); +} + +export function eserviceRiskNotFound( + eserviceId: string, + riskAnalysisId: string +): ApiError<ErrorCodes> { + return new ApiError({ + detail: `"RiskAnalysis ${riskAnalysisId} not found in Eservice ${eserviceId}"`, + code: "eserviceRiskNotFound", + title: "Risk analysis not found", + }); +} diff --git a/packages/bff/src/model/modelMappingUtils.ts b/packages/bff/src/model/modelMappingUtils.ts index 6191acad9f..04ef14355f 100644 --- a/packages/bff/src/model/modelMappingUtils.ts +++ b/packages/bff/src/model/modelMappingUtils.ts @@ -1,6 +1,5 @@ import { catalogApi, tenantApi } from "pagopa-interop-api-clients"; import { catalogApiDescriptorState } from "./api/apiTypes.js"; -import { catalogProcessApiEServiceDescriptorCertifiedAttributesSatisfied } from "./validators.js"; /* This file contains commons utility functions @@ -37,16 +36,3 @@ export function getTenantEmail( (m) => m.kind === tenantApi.MailKind.Values.CONTACT_EMAIL ); } - -export function hasCertifiedAttributes( - descriptor: catalogApi.EServiceDescriptor | undefined, - requesterTenant: tenantApi.Tenant -): boolean { - return ( - descriptor !== undefined && - catalogProcessApiEServiceDescriptorCertifiedAttributesSatisfied( - descriptor, - requesterTenant - ) - ); -} diff --git a/packages/bff/src/routers/catalogRouter.ts b/packages/bff/src/routers/catalogRouter.ts index fb4743a638..dcbc10f4fc 100644 --- a/packages/bff/src/routers/catalogRouter.ts +++ b/packages/bff/src/routers/catalogRouter.ts @@ -1,22 +1,22 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { ZodiosEndpointDefinitions } from "@zodios/core"; import { ZodiosRouter } from "@zodios/express"; +import { bffApi } from "pagopa-interop-api-clients"; import { ExpressContext, ZodiosContext, zodiosValidationErrorToApiProblem, } from "pagopa-interop-commons"; -import { bffApi } from "pagopa-interop-api-clients"; import { unsafeBrandId } from "pagopa-interop-models"; -import { PagoPAInteropBeClients } from "../providers/clientProvider.js"; -import { catalogServiceBuilder } from "../services/catalogService.js"; import { toEserviceCatalogProcessQueryParams } from "../model/api/converters/catalogClientApiConverter.js"; import { makeApiProblem } from "../model/domain/errors.js"; +import { PagoPAInteropBeClients } from "../providers/clientProvider.js"; +import { catalogServiceBuilder } from "../services/catalogService.js"; +import { fromBffAppContext } from "../utilities/context.js"; import { bffGetCatalogErrorMapper, emptyErrorMapper, } from "../utilities/errorMappers.js"; -import { fromBffAppContext } from "../utilities/context.js"; const catalogRouter = ( ctx: ZodiosContext, @@ -219,9 +219,26 @@ const catalogRouter = ( res.status(501).send() ) .put("/eservices/:eServiceId", async (_req, res) => res.status(501).send()) - .post("/eservices/:eServiceId/riskAnalysis", async (_req, res) => - res.status(501).send() - ) + .post("/eservices/:eServiceId/riskAnalysis", async (req, res) => { + const ctx = fromBffAppContext(req.ctx, req.headers); + try { + await catalogService.addRiskAnalysisToEService( + unsafeBrandId(req.params.eServiceId), + req.body, + ctx + ); + + return res.status(204).send(); + } catch (error) { + const errorRes = makeApiProblem( + error, + bffGetCatalogErrorMapper, + ctx.logger, + `Error inserting risk analysis ${req.body.name} to eservice ${req.params.eServiceId} from catalog` + ); + return res.status(errorRes.status).json(errorRes).end(); + } + }) .post("/eservices/:eServiceId/update", async (req, res) => { const ctx = fromBffAppContext(req.ctx, req.headers); try { @@ -243,16 +260,80 @@ const catalogRouter = ( }) .get( "/eservices/:eServiceId/riskAnalysis/:riskAnalysisId", - async (_req, res) => res.status(501).send() + async (req, res) => { + const ctx = fromBffAppContext(req.ctx, req.headers); + try { + const riskAnalysis = await catalogService.getEServiceRiskAnalysis( + unsafeBrandId(req.params.eServiceId), + unsafeBrandId(req.params.riskAnalysisId), + ctx + ); + + return res.status(200).json(riskAnalysis).send(); + } catch (error) { + const errorRes = makeApiProblem( + error, + bffGetCatalogErrorMapper, + ctx.logger, + `Error retrieving risk analysis ${req.params.riskAnalysisId} to eservice ${req.params.eServiceId} from catalog` + ); + return res.status(errorRes.status).json(errorRes).end(); + } + } ) .post( "/eservices/:eServiceId/riskAnalysis/:riskAnalysisId", - async (_req, res) => res.status(501).send() + async (req, res) => { + const ctx = fromBffAppContext(req.ctx, req.headers); + try { + await catalogService.updateEServiceRiskAnalysis( + unsafeBrandId(req.params.eServiceId), + unsafeBrandId(req.params.riskAnalysisId), + req.body, + ctx + ); + return res.status(204).send(); + } catch (error) { + const errorRes = makeApiProblem( + error, + bffGetCatalogErrorMapper, + ctx.logger, + `Error updating risk analysis ${req.params.riskAnalysisId} to eservice ${req.params.eServiceId} from catalog` + ); + return res.status(errorRes.status).json(errorRes).end(); + } + } ) .delete( "/eservices/:eServiceId/riskAnalysis/:riskAnalysisId", + async (req, res) => { + const ctx = fromBffAppContext(req.ctx, req.headers); + try { + await catalogService.deleteEServiceRiskAnalysis( + unsafeBrandId(req.params.eServiceId), + unsafeBrandId(req.params.riskAnalysisId), + ctx + ); + return res.status(204).send(); + } catch (error) { + const errorRes = makeApiProblem( + error, + bffGetCatalogErrorMapper, + ctx.logger, + `Error deleting risk analysis ${req.params.riskAnalysisId} to eservice ${req.params.eServiceId} from catalog` + ); + return res.status(errorRes.status).json(errorRes).end(); + } + } + ) + .get( + "/export/eservices/:eserviceId/descriptors/:descriptorId", async (_req, res) => res.status(501).send() - ); + ) + .get("/import/eservices/presignedUrl", async (_req, res) => + res.status(501).send() + ) + .post("/import/eservices", async (_req, res) => res.status(501).send()); return catalogRouter; }; diff --git a/packages/bff/src/routers/clientRouter.ts b/packages/bff/src/routers/clientRouter.ts index a9d88c6eea..df2687c39c 100644 --- a/packages/bff/src/routers/clientRouter.ts +++ b/packages/bff/src/routers/clientRouter.ts @@ -1,11 +1,11 @@ import { ZodiosEndpointDefinitions } from "@zodios/core"; import { ZodiosRouter } from "@zodios/express"; +import { bffApi } from "pagopa-interop-api-clients"; import { ExpressContext, ZodiosContext, zodiosValidationErrorToApiProblem, } from "pagopa-interop-commons"; -import { bffApi } from "pagopa-interop-api-clients"; const clientRouter = ( ctx: ZodiosContext @@ -47,10 +47,7 @@ const clientRouter = ( res.status(501).send() ) .post("/clientsConsumer", async (_req, res) => res.status(501).send()) - .post("/clientsApi", async (_req, res) => res.status(501).send()) - .get("/clients/:clientId/users/:userId/keys", async (_req, res) => - res.status(501).send() - ); + .post("/clientsApi", async (_req, res) => res.status(501).send()); return clientRouter; }; diff --git a/packages/bff/src/services/catalogService.ts b/packages/bff/src/services/catalogService.ts index b6d0a62204..fb4c99a5b4 100644 --- a/packages/bff/src/services/catalogService.ts +++ b/packages/bff/src/services/catalogService.ts @@ -6,7 +6,11 @@ import { WithLogger, formatDateyyyyMMddThhmmss, } from "pagopa-interop-commons"; -import { DescriptorId, EServiceId } from "pagopa-interop-models"; +import { + DescriptorId, + EServiceId, + RiskAnalysisId, +} from "pagopa-interop-models"; import { toBffCatalogApiDescriptorAttributes, toBffCatalogApiDescriptorDoc, @@ -16,7 +20,10 @@ import { toBffCatalogDescriptorEService, } from "../model/api/converters/catalogClientApiConverter.js"; -import { eserviceDescriptorNotFound } from "../model/domain/errors.js"; +import { + eserviceDescriptorNotFound, + eserviceRiskNotFound, +} from "../model/domain/errors.js"; import { getLatestActiveDescriptor } from "../model/modelMappingUtils.js"; import { assertRequesterIsProducer } from "../model/validators.js"; import { @@ -118,6 +125,20 @@ export const retrieveEserviceDescriptor = ( return descriptor; }; +const retrieveRiskAnalysis = ( + eservice: catalogApi.EService, + riskAnalysisId: string +): catalogApi.EServiceRiskAnalysis => { + const riskAnalysis = eservice.riskAnalysis.find( + (ra) => ra.id === riskAnalysisId + ); + + if (!riskAnalysis) { + throw eserviceRiskNotFound(eservice.id, riskAnalysisId); + } + return riskAnalysis; +}; + const getAttributeIds = ( descriptor: catalogApi.EServiceDescriptor ): string[] => [ @@ -490,5 +511,64 @@ export function catalogServiceBuilder( file: Buffer.from(buildCsv(consumers)), }; }, + updateEServiceRiskAnalysis: async ( + eserviceId: EServiceId, + riskAnalysisId: RiskAnalysisId, + riskAnalysisSeed: bffApi.EServiceRiskAnalysisSeed, + context: WithLogger<BffAppContext> + ): Promise<void> => + await catalogProcessClient.updateRiskAnalysis(riskAnalysisSeed, { + headers: context.headers, + params: { + eServiceId: eserviceId, + riskAnalysisId, + }, + }), + deleteEServiceRiskAnalysis: async ( + eserviceId: EServiceId, + riskAnalysisId: RiskAnalysisId, + context: WithLogger<BffAppContext> + ): Promise<void> => + await catalogProcessClient.deleteRiskAnalysis(undefined, { + headers: context.headers, + params: { + eServiceId: eserviceId, + riskAnalysisId, + }, + }), + addRiskAnalysisToEService: async ( + eserviceId: EServiceId, + riskAnalysisSeed: bffApi.EServiceRiskAnalysisSeed, + context: WithLogger<BffAppContext> + ): Promise<void> => + await catalogProcessClient.createRiskAnalysis( + { + name: riskAnalysisSeed.name, + riskAnalysisForm: riskAnalysisSeed.riskAnalysisForm, + }, + { + headers: context.headers, + params: { + eServiceId: eserviceId, + }, + } + ), + getEServiceRiskAnalysis: async ( + eserviceId: EServiceId, + riskAnalysisId: RiskAnalysisId, + context: WithLogger<BffAppContext> + ): Promise<bffApi.EServiceRiskAnalysis> => { + const eservice: catalogApi.EService = + await catalogProcessClient.getEServiceById({ + params: { + eServiceId: eserviceId, + }, + headers: context.headers, + }); + + const riskAnalysis = retrieveRiskAnalysis(eservice, riskAnalysisId); + + return toBffCatalogApiEserviceRiskAnalysis(riskAnalysis); + }, }; } diff --git a/packages/bff/src/utilities/errorMappers.ts b/packages/bff/src/utilities/errorMappers.ts index 4432e4790a..fc5f43c207 100644 --- a/packages/bff/src/utilities/errorMappers.ts +++ b/packages/bff/src/utilities/errorMappers.ts @@ -14,7 +14,11 @@ const { export const bffGetCatalogErrorMapper = (error: ApiError<ErrorCodes>): number => match(error.code) - .with("descriptorNotFound", () => HTTP_STATUS_NOT_FOUND) + .with( + "descriptorNotFound", + "eserviceRiskNotFound", + () => HTTP_STATUS_NOT_FOUND + ) .with("invalidEserviceRequester", () => HTTP_STATUS_FORBIDDEN) .otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR);