Skip to content
Open
6 changes: 6 additions & 0 deletions .changeset/dirty-phones-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"io-services-cms-webapp": minor
"@io-services-cms/models": patch
---

Add Upsert and Get Service activation API
19 changes: 19 additions & 0 deletions apps/io-services-cms-webapp/GetServiceActivation/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"route": "activations",
"methods": ["post"]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
],
"scriptFile": "../dist/main.js",
"entryPoint": "httpEntryPoint"
}
19 changes: 19 additions & 0 deletions apps/io-services-cms-webapp/UpsertServiceActivation/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"route": "activations",
"methods": ["put"]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
],
"scriptFile": "../dist/main.js",
"entryPoint": "httpEntryPoint"
}
93 changes: 93 additions & 0 deletions apps/io-services-cms-webapp/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,93 @@ paths:
description: Too many requests
'500':
description: Internal server error
/activations/:
post:
tags:
- activations
operationId: getServiceActivationByPOST
summary: Get a Service Activation for a User
description: Returns the current Activation for a couple Service/User
parameters:
- $ref: '#/components/parameters/UserGroups'
- $ref: '#/components/parameters/SubscriptionID'
- $ref: '#/components/parameters/UserEmail'
- $ref: '#/components/parameters/UserID'
requestBody:
description: A fiscal code payload
content:
application/json:
schema:
$ref: '#/components/schemas/FiscalCodePayload'
required: true
responses:
'200':
description: Service Activation fetched successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Activation'
'401':
description: Unauthorized
'403':
description: Forbidden
'404':
description: No user activation found for the provided fiscal code
content:
application/json:
schema:
$ref: '#/components/schemas/ResponseError'
'429':
description: Too many requests
'500':
description: Internal server error retrieving the Activation
content:
application/json:
schema:
$ref: '#/components/schemas/ResponseError'
put:
tags:
- activations
operationId: upsertServiceActivation
summary: Upsert a Service Activation for a User
description: Create or update an Activation for a couple Service/User
parameters:
- $ref: '#/components/parameters/UserGroups'
- $ref: '#/components/parameters/SubscriptionID'
- $ref: '#/components/parameters/UserEmail'
- $ref: '#/components/parameters/UserID'
requestBody:
description: A special service's activation body payload
content:
application/json:
schema:
$ref: '#/components/schemas/ActivationPayload'
required: true
responses:
'200':
description: Service Activation created or updated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Activation'
'401':
description: Unauthorized
'403':
description: Forbidden
'404':
description: No user activation found for the provided fiscal code.
content:
application/json:
schema:
$ref: '#/components/schemas/ResponseError'
'429':
description: Too many requests
'500':
description: The activation cannot be created or updated
content:
application/json:
schema:
$ref: '#/components/schemas/ResponseError'
components:
parameters:
UserEmail:
Expand Down Expand Up @@ -858,3 +945,9 @@ components:
$ref: 'https://raw.githubusercontent.com/pagopa/io-services-cms/master/packages/io-services-cms-models/openapi/services-cms-schemas.yaml#/ReviewRequest'
Timestamp:
$ref: 'https://raw.githubusercontent.com/pagopa/io-services-cms/master/packages/io-services-cms-models/openapi/commons-schemas.yaml#/Timestamp'
Activation:
$ref: '../../packages/io-services-cms-models/openapi/services-cms-schemas.yaml#/Activation'
ActivationPayload:
$ref: '../../packages/io-services-cms-models/openapi/services-cms-schemas.yaml#/ActivationPayload'
FiscalCodePayload:
$ref: '../../packages/io-services-cms-models/openapi/services-cms-schemas.yaml#/FiscalCodePayload'
9 changes: 7 additions & 2 deletions apps/io-services-cms-webapp/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ const legacyServicesContainer = cosmosdbClient

const legacyServiceModel = new ServiceModel(legacyServicesContainer);

const blobService = createBlobService(config.ASSET_STORAGE_CONNECTIONSTRING);
const assetBlobService = createBlobService(
config.ASSET_STORAGE_CONNECTIONSTRING,
);

// eventhub producer for ServicePublication
const servicePublicationEventHubProducer = new EventHubProducerClient(
Expand Down Expand Up @@ -221,9 +223,12 @@ const blobServiceClient = new BlobServiceClient(
// entrypoint for all http functions
export const httpEntryPoint = pipe(
{
activationsContainerClient: blobServiceClient.getContainerClient(
config.ACTIVATIONS_CONTAINER_NAME,
),
apimService,
assetBlobService,
basePath: BASE_PATH,
blobService,
config,
fsmLifecycleClientCreator,
fsmPublicationClient,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import * as E from "fp-ts/lib/Either";
import { ResponseErrorForbiddenNotAuthorized } from "@pagopa/ts-commons/lib/responses";
import { authorizedForSpecialServicesTask } from "../special-services";

describe("authorizedForSpecialServicesTask", () => {
it("should return the category if it is SPECIAL_CATEGORY", async () => {
const result = await authorizedForSpecialServicesTask("SPECIAL")();
expect(E.isRight(result)).toBe(true);
if (E.isRight(result)) {
expect(result.right).toBe("SPECIAL");
}
});

it("should return a forbidden error if the category is not SPECIAL_CATEGORY", async () => {
const result = await authorizedForSpecialServicesTask("STANDARD")();
expect(E.isLeft(result)).toBe(true);
if (E.isLeft(result)) {
expect(result.left).toEqual(ResponseErrorForbiddenNotAuthorized);
}
});

it("should return a forbidden error if the category is undefined", async () => {
const result = await authorizedForSpecialServicesTask(undefined)();
expect(E.isLeft(result)).toBe(true);
if (E.isLeft(result)) {
expect(result.left).toEqual(ResponseErrorForbiddenNotAuthorized);
}
});
});
2 changes: 2 additions & 0 deletions apps/io-services-cms-webapp/src/utils/applicationinsight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export enum EventNameEnum {
CreateService = "create",
DeleteService = "delete",
EditService = "edit",
GetServiceActivation = "activation.get",
GetServiceHistory = "history.get",
GetServiceKeys = "keys.get",
GetServiceLifecycle = "lifecycle.get",
Expand All @@ -56,4 +57,5 @@ export enum EventNameEnum {
RegenerateServiceKeys = "keys.regenerate",
UnpublishService = "unpublish",
UploadServiceLogo = "logo.put",
UpsertServiceActivation = "activation.upsert",
}
24 changes: 24 additions & 0 deletions apps/io-services-cms-webapp/src/utils/special-services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ServiceLifecycle } from "@io-services-cms/models";
import {
IResponseErrorForbiddenNotAuthorized,
ResponseErrorForbiddenNotAuthorized,
} from "@pagopa/ts-commons/lib/responses";
import * as O from "fp-ts/lib/Option";
import * as TE from "fp-ts/lib/TaskEither";
import { pipe } from "fp-ts/lib/function";

const SPECIAL_CATEGORY: ServiceLifecycle.definitions.Service["data"]["metadata"]["category"] =
"SPECIAL";

// Authorization check: only services of SPECIAL_CATEGORY can proceed
export const authorizedForSpecialServicesTask = (
category: ServiceLifecycle.definitions.Service["data"]["metadata"]["category"],
): TE.TaskEither<
IResponseErrorForbiddenNotAuthorized,
ServiceLifecycle.definitions.Service["data"]["metadata"]["category"]
> =>
pipe(
O.fromNullable(category),
O.filter((cat) => cat === SPECIAL_CATEGORY),
TE.fromOption(() => ResponseErrorForbiddenNotAuthorized),
);
Comment on lines +10 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion(non-blocking): we can refactor it into a more understandable form

Suggested change
const SPECIAL_CATEGORY: ServiceLifecycle.definitions.Service["data"]["metadata"]["category"] =
"SPECIAL";
// Authorization check: only services of SPECIAL_CATEGORY can proceed
export const authorizedForSpecialServicesTask = (
category: ServiceLifecycle.definitions.Service["data"]["metadata"]["category"],
): TE.TaskEither<
IResponseErrorForbiddenNotAuthorized,
ServiceLifecycle.definitions.Service["data"]["metadata"]["category"]
> =>
pipe(
O.fromNullable(category),
O.filter((cat) => cat === SPECIAL_CATEGORY),
TE.fromOption(() => ResponseErrorForbiddenNotAuthorized),
);
// Authorization check: only services of SPECIAL_CATEGORY can proceed
export const authorizedForSpecialServicesTask = (
category: ServiceLifecycle.definitions.Service["data"]["metadata"]["category"],
): TE.TaskEither<IResponseErrorForbiddenNotAuthorized, "SPECIAL"> =>
pipe(
O.fromNullable(category),
O.filter((cat) => cat === "SPECIAL"),
TE.fromOption(() => ResponseErrorForbiddenNotAuthorized),
);

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const {
itemToResponseResponseMock,
logErrorResponseMock,
getLoggerMock,
cosmosdbInstanceMock,
} = vi.hoisted(() => {
//const payloadToItemResponseMock = "payloadToItemResponse";
const payloadToItemResponseMock = {
Expand All @@ -47,6 +48,9 @@ const {
itemToResponseResponseMock,
logErrorResponseMock,
getLoggerMock: vi.fn(() => ({ logErrorResponse: logErrorResponseMock })),
cosmosdbInstanceMock: {
container: vi.fn(() => ({})),
},
};
});

Expand All @@ -63,6 +67,10 @@ vi.mock("../../../utils/logger", () => ({
getLogger: getLoggerMock,
}));

vi.mock("../../../utils/cosmos-legacy", () => ({
cosmosdbInstance: cosmosdbInstanceMock,
}));

const aNewService = {
name: "a service",
description: "a description",
Expand Down Expand Up @@ -147,7 +155,6 @@ describe("createService", () => {
fsmPublicationClient: vi.fn(),
subscriptionCIDRsModel,
telemetryClient: mockAppinsights,
blobService: vi.fn(),
serviceTopicDao: vi.fn(),
} as unknown as WebServerDependencies);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { IConfig } from "../../../config";
import { WebServerDependencies, createWebServer } from "../../index";

vi.mock("../../../utils/cosmos-legacy", () => ({
cosmosdbInstance: {
container: vi.fn(() => ({})),
},
}));

const serviceLifecycleStore =
stores.createMemoryStore<ServiceLifecycle.ItemType>();
const fsmLifecycleClientCreator = ServiceLifecycle.getFsmClient(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const {
getLoggerMock,
fsmLifecycleClientMock,
fsmLifecycleClientCreatorMock,
cosmosdbInstanceMock,
} = vi.hoisted(() => {
const payloadToItemResponseMock = "payloadToItemResponse";
const itemToResponseResponseMock = { value: "itemToResponseResponse" };
Expand Down Expand Up @@ -84,6 +85,9 @@ const {
getLoggerMock: vi.fn(() => ({ logErrorResponse: logErrorResponseMock })),
fsmLifecycleClientMock,
fsmLifecycleClientCreatorMock: vi.fn(() => fsmLifecycleClientMock),
cosmosdbInstanceMock: {
container: vi.fn(() => ({})),
},
};
});

Expand All @@ -104,6 +108,10 @@ vi.mock("../../../utils/logger", () => ({
getLogger: getLoggerMock,
}));

vi.mock("../../../utils/cosmos-legacy", () => ({
cosmosdbInstance: cosmosdbInstanceMock,
}));

const aManageSubscriptionId = "MANAGE-123";
const anUserId = "123";

Expand Down
Loading
Loading