Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 53 additions & 6 deletions docs/index.html

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/openapi/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ paths:
$ref: './site-opportunities.yaml#/site-opportunity-suggestions-auto-fix'
/sites/{siteId}/site-enrollments:
$ref: './site-enrollments-api.yaml#/site-enrollments-by-site'
/sites/{siteId}/site-enrollments/{enrollmentId}/config:
$ref: './site-enrollments-api.yaml#/site-enrollment-config'
/sites/{siteId}/user-activities:
$ref: './user-activities-api.yaml#/paths/~1sites~1{siteId}~1user-activities'
/sites/{siteId}/key-events:
Expand Down
8 changes: 8 additions & 0 deletions docs/openapi/schemas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3642,13 +3642,21 @@ SiteEnrollment:
updatedBy:
description: Who last updated the site enrollment
$ref: '#/Id'
config:
description: Configuration object with dynamic string key-value pairs for the site enrollment
type: object
additionalProperties:
type: string
example:
id: "123e4567-e89b-12d3-a456-426614174000"
siteId: "987fcdeb-51a2-43d7-9876-543210987654"
entitlementId: "456e4567-e89b-12d3-a456-426614174000"
createdAt: "2024-01-15T10:30:00Z"
updatedAt: "2024-01-15T10:30:00Z"
updatedBy: "system"
config:
dataFolder: "example-site-data"
brand: "Example Brand"

TrialUser:
type: object
Expand Down
86 changes: 86 additions & 0 deletions docs/openapi/site-enrollments-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,90 @@ site-enrollments-by-site:
security:
- ims_key: [ ]

site-enrollment-config:
parameters:
- $ref: './parameters.yaml#/siteId'
- name: enrollmentId
in: path
description: The unique identifier of the site enrollment
required: true
schema:
$ref: './schemas.yaml#/Id'
get:
tags:
- site-enrollments
summary: Get configuration for a specific site enrollment
description: |
This endpoint retrieves the configuration object for a specific site enrollment.
operationId: getSiteEnrollmentConfig
responses:
'200':
description: Site enrollment configuration retrieved successfully
content:
application/json:
schema:
type: object
additionalProperties:
type: string
example:
dataFolder: "example-site-data"
brand: "Example Brand"
'400':
$ref: './responses.yaml#/400-no-site-id'
'401':
$ref: './responses.yaml#/401'
'403':
$ref: './responses.yaml#/403-site-access-forbidden'
'404':
$ref: './responses.yaml#/404'
'500':
$ref: './responses.yaml#/500'
security:
- ims_key: [ ]
put:
tags:
- site-enrollments
summary: Update configuration for a specific site enrollment
description: |
This endpoint updates the configuration object for a specific site enrollment.
The configuration should be an object with string key-value pairs.
operationId: updateSiteEnrollmentConfig
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties:
type: string
example:
dataFolder: "updated-site-data"
brand: "Updated Brand"
customKey: "customValue"
responses:
'200':
description: Site enrollment configuration updated successfully
content:
application/json:
schema:
type: object
additionalProperties:
type: string
example:
dataFolder: "updated-site-data"
brand: "Updated Brand"
customKey: "customValue"
'400':
$ref: './responses.yaml#/400'
'401':
$ref: './responses.yaml#/401'
'403':
$ref: './responses.yaml#/403-site-access-forbidden'
'404':
$ref: './responses.yaml#/404'
'500':
$ref: './responses.yaml#/500'
security:
- ims_key: [ ]


125 changes: 124 additions & 1 deletion src/controllers/site-enrollments.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import {
isNonEmptyObject,
isValidUUID,
isObject,
} from '@adobe/spacecat-shared-utils';

import { SiteEnrollmentDto } from '../dto/site-enrollment.js';
Expand All @@ -45,6 +46,25 @@ function SiteEnrollmentsController(ctx) {

const accessControlUtil = AccessControlUtil.fromContext(ctx);

/**
* Validates config object to ensure it contains only string key-value pairs.
* @param {any} config - The config object to validate.
* @returns {boolean} True if valid, false otherwise.
*/
const validateConfig = (config) => {
if (!config) return true; // Allow null/undefined config
if (!isObject(config)) return false;

// Check that all keys and values are strings
// Also ensure keys are not numeric (even if converted to strings)
return Object.entries(config).every(([key, value]) => {
// Check if the key is a string and not a numeric string
const isValidKey = typeof key === 'string' && !/^\d+$/.test(key);
const isValidValue = typeof value === 'string';
return isValidKey && isValidValue;
});
};

/**
* Gets site enrollments by site ID.
* @param {object} context - Context of the request.
Expand All @@ -70,7 +90,7 @@ function SiteEnrollmentsController(ctx) {

const siteEnrollments = await SiteEnrollment.allBySiteId(siteId);
const enrollments = siteEnrollments.map(
(enrollment) => SiteEnrollmentDto.toJSON(enrollment),
(siteEnrollment) => SiteEnrollmentDto.toJSON(siteEnrollment),
);
return ok(enrollments);
} catch (e) {
Expand All @@ -79,8 +99,111 @@ function SiteEnrollmentsController(ctx) {
}
};

/**
* Gets configuration for a specific site enrollment.
* @param {object} context - Context of the request.
* @returns {Promise<Response>} Site enrollment config response.
*/
const getConfigByEnrollmentID = async (context) => {
const { siteId, enrollmentId } = context.params;

if (!isValidUUID(siteId)) {
return badRequest('Site ID required');
}

if (!isValidUUID(enrollmentId)) {
return badRequest('Enrollment ID required');
}

try {
// Check if user has access to the site
const site = await Site.findById(siteId);
if (!site) {
return notFound('Site not found');
}

if (!await accessControlUtil.hasAccess(site)) {
return forbidden('Access denied to this site');
}

// Find the specific site enrollment
const siteEnrollment = await SiteEnrollment.findById(enrollmentId);
if (!siteEnrollment) {
return notFound('Site enrollment not found');
}

// Verify the site enrollment belongs to the specified site
if (siteEnrollment.getSiteId() !== siteId) {
return notFound('Site enrollment not found for this site');
}

const config = siteEnrollment.getConfig() || {};
return ok(config);
} catch (e) {
context.log.error(`Error getting site enrollment config for siteEnrollment ${enrollmentId}: ${e.message}`);
return internalServerError(e.message);
}
};

/**
* Updates configuration for a specific site enrollment.
* @param {object} context - Context of the request.
* @returns {Promise<Response>} Updated site enrollment config response.
*/
const updateConfigByEnrollmentID = async (context) => {
const { siteId, enrollmentId } = context.params;
const { data: config } = context;

if (!isValidUUID(siteId)) {
return badRequest('Site ID required');
}

if (!isValidUUID(enrollmentId)) {
return badRequest('Enrollment ID required');
}

if (!validateConfig(config)) {
return badRequest('Config must be an object with string key-value pairs');
}

try {
// Check if user has access to the site
const site = await Site.findById(siteId);
if (!site) {
return notFound('Site not found');
}

if (!await accessControlUtil.hasAccess(site)) {
return forbidden('Access denied to this site');
}

// Find the specific site enrollment
const siteEnrollment = await SiteEnrollment.findById(enrollmentId);
if (!siteEnrollment) {
return notFound('Site enrollment not found');
}

// Verify the site enrollment belongs to the specified site
if (siteEnrollment.getSiteId() !== siteId) {
return notFound('Site enrollment not found for this site');
}

// Update the config
siteEnrollment.setConfig(config || {});
await siteEnrollment.save();

const updatedConfig = siteEnrollment.getConfig() || {};
return ok(updatedConfig);
} catch (e) {
context.log.error(`Error updating site enrollment config for siteEnrollment ${enrollmentId}: ${e.message}`);
return internalServerError(e.message);
}
};

return {
getBySiteID,
getConfigByEnrollmentID,
updateConfigByEnrollmentID,
};
}

Expand Down
4 changes: 3 additions & 1 deletion src/dto/site-enrollment.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export const SiteEnrollmentDto = {
* entitlementId: string,
* createdAt: string,
* updatedAt: string,
* updatedBy: string
* updatedBy: string,
* config: Record<string, string>
* }}
*/
toJSON: (siteEnrollment) => ({
Expand All @@ -33,5 +34,6 @@ export const SiteEnrollmentDto = {
createdAt: siteEnrollment.getCreatedAt(),
updatedAt: siteEnrollment.getUpdatedAt(),
updatedBy: siteEnrollment.getUpdatedBy(),
config: siteEnrollment.getConfig(),
}),
};
2 changes: 2 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@ export default function getRouteHandlers(
'GET /sites/:siteId/user-activities': userActivityController.getBySiteID,
'POST /sites/:siteId/user-activities': userActivityController.createTrialUserActivity,
'GET /sites/:siteId/site-enrollments': siteEnrollmentController.getBySiteID,
'GET /sites/:siteId/site-enrollments/:enrollmentId/config': siteEnrollmentController.getConfigByEnrollmentID,
'PUT /sites/:siteId/site-enrollments/:enrollmentId/config': siteEnrollmentController.updateConfigByEnrollmentID,
'GET /organizations/:organizationId/trial-users': trialUserController.getByOrganizationID,
'POST /organizations/:organizationId/trial-user-invite': trialUserController.createTrialUserForEmailInvite,
'GET /organizations/:organizationId/entitlements': entitlementController.getByOrganizationID,
Expand Down
Loading