diff --git a/.github/workflows/pull-request-from-branch-check.yaml b/.github/workflows/pull-request-from-branch-check.yaml new file mode 100644 index 0000000..77d1970 --- /dev/null +++ b/.github/workflows/pull-request-from-branch-check.yaml @@ -0,0 +1,18 @@ +name: Main Branch Protection + +on: + pull_request: + branches: + - main + +jobs: + check-branch: + runs-on: ubuntu-latest + steps: + - name: Check branch + run: | + if [[ ${GITHUB_HEAD_REF} != development ]] && [[ ${GITHUB_HEAD_REF} != documentation ]] && ! [[ ${GITHUB_HEAD_REF} =~ ^hotfix/ ]]; + then + echo "Error: Pull request must come from 'development', 'documentation' or 'hotfix/' branch" + exit 1 + fi diff --git a/.github/workflows/pull-request-lint-check.yaml b/.github/workflows/pull-request-lint-check.yaml new file mode 100644 index 0000000..4a8c874 --- /dev/null +++ b/.github/workflows/pull-request-lint-check.yaml @@ -0,0 +1,21 @@ +name: Lint Check + +on: + pull_request: + branches: + - development + - main + +jobs: + lint-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install dependencies + run: npm i + + - name: Linting + run: npm run lint diff --git a/README.md b/README.md index baa1e16..fa2d566 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # OpenConector Provides gateway and service bus functionality like mapping, translation and synchronisation of data + diff --git a/appinfo/routes.php b/appinfo/routes.php index 27548e5..f96805d 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -22,6 +22,8 @@ ['name' => 'synchronizations#test', 'url' => '/api/synchronizations-test/{id}', 'verb' => 'POST'], // Mapping endpoints ['name' => 'mappings#test', 'url' => '/api/mappings/test', 'verb' => 'POST'], + ['name' => 'mappings#saveObject', 'url' => '/api/mappings/objects', 'verb' => 'POST'], + ['name' => 'mappings#getObjects', 'url' => '/api/mappings/objects', 'verb' => 'GET'], // Running endpoints ['name' => 'endpoints#run', 'url' => '/api/v1/{endpoint}', 'verb' => 'GET'], ['name' => 'endpoints#run', 'url' => '/api/v1/{endpoint}', 'verb' => 'PUT'], diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..d3bb763 --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,88 @@ +# Authentication on sources + +In order to authenticate on other sources there are possibilities based upon the way the source expects autentication parameters. +These parameters can be set in the source configuration. For example, if the source expects an API key in the headers, we can set the parameter `headers.Authorization` with the API key as value. + +Usually, sources tend to use dynamic authorization parameters in order to prevent the same authentication parameter from being used by adversaries that catch a call and deduce the parameter. + +At the moment, OpenConnector supports two dynamic authentication methods, OAuth and JWT Bearers. + +## OAuth + +To use OAuth we put in our Authorization header the following value: +```twig +Bearer {{ oauthToken(source) }} +``` +This will impose an OAuth 2.0 access token after `Bearer` if the field `authenticationConfig` contains correct values. +OpenConnector supports the OAuth 2.0 protocol with client credentials and password credentials as grant_types. + +>[!NOTE] +> TODO: How to add authenticationConfig parameters in frontend + +When using OAuth, OpenConnector supports the following parameters: + +### Standard parameters +* `grant_type`: The type of grant we have to use at the source. Supported are `client_credentials` and `password` +* `scope`: The scope(s) needed to perform the requests we want to do in the API. +* `tokenUrl`: The URL used to fetch the actual access token. Usually this url can be recognised by its path ending on `/oauth/token` +* `authentication`: Location of the credentials, either `body` for credentials included in the request body, or `basic_auth` when the credentials have to be sent as a basic_auth header. + > Only used when `grant_type` is `client_credentials` +* `client_id`: The client id of the OAuth client + > Only used when `grant_type` is `client_credentials` +* `client_secret`: The secret for the OAuth client + > Only used when `grant_type` is `client_credentials` +* `username`: The username for the OAuth client + > Only used when `grant_type` is `password` +* `password`: The password for the OAuth client + > Only used when `grant_type` is `password` + +This results in the following example: +```json +{ + "grant_type": "client_credentials", + "scope": "api", + "authentication": "body", + "tokenUrl": "https://example.com/oauth/token", + "client_id": "test-client", + "client_secret": "some secret value" +} +``` +### Custom parameters + +> [!WARNING] +> Custom parameters are currently in beta, it is not recommended to use them in production environments. + +At the moment, OpenConnector is tested with the following custom parameters: + +* `client_assertion_type`, only meaningful at the moment when value is set to `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. When this is set (for Microsoft authentications) the following fields are needed to generate the `client-assertion`-field + - `private_key`: The base64 encoded private key of the certificate uploaded to Microsoft. + - `x5t`: The base64 encoded sha1 fingerprint of the uploaded certificate, generated by running the following command: + + ```bash + echo $(openssl x509 -in certificate.crt -fingerprint -noout) | sed 's/SHA1 Fingerprint=//g' | sed 's/://g' | xxd -r -ps | base64`) + ``` + + - `payload`: The payload of the JWT generated as `client_assertion`, this can contain Twig variables to render, for example to set timestamps in the JWT payload. + +## JWT Bearer + +A second supported way of using dynamic authentication is setting a JWT Bearer. This means setting a header or query parameter with a JWT token. + +This can for example be used by setting an Authorization header with the following value: +```twig +Bearer {{ jwtToken(source) }} +``` + +This will impose a JWT token after the bearer. For this, the `authenticationConfig` field of the source needs to contain the following fields: +* `algorithm`: The algorithm that should be used to generate the JWT. Supported are `HS256`, `HS384` and `HS512` for HMAC algorithms, `RS256`, `RS384`, `RS512` and `PS256` for RSA algorithms. +* `secret`: The secret used for the JWT. This can either be a HMAC shared secret, or a RSA private key in base64 encoding. +* `payload`: The payload of your JWT, json_encoded. + +This results in the following example for the `authenticationConfig` parameter in i.e. an OpenZaak source. +```json +{ + "algorithm": "HS256", + "secret": "YOUR_256BIT_(32BYTE)_HMAC_SECRET", + "payload": "{\"iss\":\"my_zgw_client\",\"iat\":{{ 'now'|date('U') }},\"client_id\":\"my_zgw_client\",\"user_id\":\"my_zgw_client\",\"user_representation\":\"me@company.com\",\"aud\":\"my_zgw_client\"}" +} +``` diff --git a/lib/Controller/MappingsController.php b/lib/Controller/MappingsController.php index f3ab903..190d83b 100644 --- a/lib/Controller/MappingsController.php +++ b/lib/Controller/MappingsController.php @@ -16,6 +16,8 @@ use OCP\IAppConfig; use OCP\IRequest; use OCP\IURLGenerator; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; class MappingsController extends Controller { @@ -31,7 +33,8 @@ public function __construct( IRequest $request, private readonly IAppConfig $config, private readonly MappingMapper $mappingMapper, - private readonly MappingService $mappingService + private readonly MappingService $mappingService, + private readonly ObjectService $objectService ) { parent::__construct($appName, $request); @@ -125,17 +128,17 @@ public function create(): JSONResponse return new JSONResponse($this->mappingMapper->createFromArray(object: $data)); } - /** - * Updates an existing mapping - * - * This method updates an existing mapping based on its ID. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @param string $id The ID of the mapping to update - * @return JSONResponse A JSON response containing the updated mapping details - */ + /** + * Updates an existing mapping + * + * This method updates an existing mapping based on its ID. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param int $id The ID of the mapping to update + * @return JSONResponse A JSON response containing the updated mapping details + */ public function update(int $id): JSONResponse { $data = $this->request->getParams(); @@ -151,17 +154,18 @@ public function update(int $id): JSONResponse return new JSONResponse($this->mappingMapper->updateFromArray(id: (int) $id, object: $data)); } - /** - * Deletes a mapping - * - * This method deletes a mapping based on its ID. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @param string $id The ID of the mapping to delete - * @return JSONResponse An empty JSON response - */ + /** + * Deletes a mapping + * + * This method deletes a mapping based on its ID. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param int $id The ID of the mapping to delete + * @return JSONResponse An empty JSON response + * @throws \OCP\DB\Exception + */ public function destroy(int $id): JSONResponse { $this->mappingMapper->delete($this->mappingMapper->find((int) $id)); @@ -169,42 +173,47 @@ public function destroy(int $id): JSONResponse return new JSONResponse([]); } - /** - * Tests a mapping - * - * This method tests a mapping with provided input data and optional schema validation. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @return JSONResponse A JSON response containing the test results - * - * @example - * Request: - * { - * "inputObject": "{\"name\":\"John Doe\",\"age\":30,\"email\":\"john@example.com\"}", - * "mapping": { - * "mapping": { - * "fullName":"{{name}}", - * "userAge":"{{age}}", - * "contactEmail":"{{email}}" - * } - * }, - * "schema": "user_schema_id", - * "validation": true - * } - * - * Response: - * { - * "resultObject": { - * "fullName": "John Doe", - * "userAge": 30, - * "contactEmail": "john@example.com" - * }, - * "isValid": true, - * "validationErrors": [] - * } - */ + /** + * Tests a mapping + * + * This method tests a mapping with provided input data and optional schema validation. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param ObjectService $objectService + * @param IURLGenerator $urlGenerator + * + * @return JSONResponse A JSON response containing the test results + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * + * @example + * Request: + * { + * "inputObject": "{\"name\":\"John Doe\",\"age\":30,\"email\":\"john@example.com\"}", + * "mapping": { + * "mapping": { + * "fullName":"{{name}}", + * "userAge":"{{age}}", + * "contactEmail":"{{email}}" + * } + * }, + * "schema": "user_schema_id", + * "validation": true + * } + * + * Response: + * { + * "resultObject": { + * "fullName": "John Doe", + * "userAge": 30, + * "contactEmail": "john@example.com" + * }, + * "isValid": true, + * "validationErrors": [] + * } + */ public function test(ObjectService $objectService, IURLGenerator $urlGenerator): JSONResponse { $openRegisters = $objectService->getOpenRegisters(); @@ -291,4 +300,57 @@ public function test(ObjectService $objectService, IURLGenerator $urlGenerator): 'validationErrors' => $validationErrors ]); } + + /** + * Saves a mapping object + * + * This method saves a mapping object based on POST data. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse|null + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function saveObject(): ?JSONResponse + { + // Check if the OpenRegister service is available + $openRegisters = $this->objectService->getOpenRegisters(); + if ($openRegisters !== null) { + $data = $this->request->getParams(); + return new JSONResponse($openRegisters->saveObject($data['register'], $data['schema'], $data['object'])); + } + + return null; + } + + /** + * Retrieves a list of objects to map to + * + * This method retrieves a list of objects to map to based on GET data. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function getObjects(): JSONResponse + { + // Check if the OpenRegister service is available + $openRegisters = $this->objectService->getOpenRegisters(); + $data = []; + if ($openRegisters !== null) { + $data['openRegisters'] = true; + $data['availableRegisters'] = $openRegisters->getRegisters(); + } + else { + $data['openRegisters'] = false; + } + + return new JSONResponse($data); + + } } diff --git a/lib/Db/CallLog.php b/lib/Db/CallLog.php index c8c40c8..9dc69b5 100644 --- a/lib/Db/CallLog.php +++ b/lib/Db/CallLog.php @@ -80,8 +80,9 @@ public function jsonSerialize(): array 'synchronizationId' => $this->synchronizationId, 'userId' => $this->userId, 'sessionId' => $this->sessionId, - 'expires' => $this->expires, - 'created' => $this->created, + 'expires' => isset($this->expires) ? $this->expires->format('c') : null, + 'created' => isset($this->created) ? $this->created->format('c') : null, + ]; } } diff --git a/lib/Service/AuthenticationService.php b/lib/Service/AuthenticationService.php index b6518ed..b59e41b 100644 --- a/lib/Service/AuthenticationService.php +++ b/lib/Service/AuthenticationService.php @@ -327,6 +327,7 @@ public function fetchJWTToken (array $configuration): string } $payload = $this->getJWTPayload($configuration); + $jwk = $this->getJWK($configuration); if ($jwk === null) { diff --git a/lib/Service/CallService.php b/lib/Service/CallService.php index 4e7ca38..4a86c60 100644 --- a/lib/Service/CallService.php +++ b/lib/Service/CallService.php @@ -197,6 +197,11 @@ public function call( // Set authentication if needed. @todo: create the authentication service //$createCertificates && $this->getCertificate($config); + // Make sure to filter out all the authentication variables / secrets. + $config = array_filter($config, function ($key) { + return str_contains(strtolower($key), 'authentication') === false; + }, ARRAY_FILTER_USE_KEY); + // Let's log the call. $this->source->setLastCall(new \DateTime()); // @todo: save the source diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index ca02cf1..eb5f2c0 100644 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -6,6 +6,7 @@ use Exception; use GuzzleHttp\Client; use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\GuzzleException; use OCP\App\IAppManager; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; @@ -49,7 +50,7 @@ public function getClient(array $config): Client * @param array $config The configuration that should be used by the call. * * @return array The resulting object. - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws GuzzleException */ public function saveObject(array $data, array $config): array { @@ -81,7 +82,7 @@ public function saveObject(array $data, array $config): array * * @return array The objects found for given filters. * - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws GuzzleException */ public function findObjects(array $filters, array $config): array { @@ -115,7 +116,7 @@ public function findObjects(array $filters, array $config): array * * @return array The resulting object. * - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws GuzzleException */ public function findObject(array $filters, array $config): array { @@ -149,7 +150,7 @@ public function findObject(array $filters, array $config): array * * @return array The updated object. * - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws GuzzleException */ public function updateObject(array $filters, array $update, array $config): array { @@ -181,7 +182,7 @@ public function updateObject(array $filters, array $update, array $config): arra * * @return array An empty array. * - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws GuzzleException */ public function deleteObject(array $filters, array $config): array { @@ -206,7 +207,7 @@ public function deleteObject(array $filters, array $config): array * @param array $pipeline The pipeline to use. * @param array $config The configuration to use in the call. * @return array - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws GuzzleException */ public function aggregateObjects(array $filters, array $pipeline, array $config):array { diff --git a/lib/Service/SynchronizationService.php b/lib/Service/SynchronizationService.php index c4afe47..37fcb83 100644 --- a/lib/Service/SynchronizationService.php +++ b/lib/Service/SynchronizationService.php @@ -83,6 +83,9 @@ public function __construct( */ public function synchronize(Synchronization $synchronization, ?bool $isTest = false): array { + if (empty($synchronization->getSourceId()) === true) { + throw new Exception('sourceId of synchronziation cannot be empty. Canceling synchronization..'); + } $objectList = $this->getAllObjectsFromSource(synchronization: $synchronization, isTest: $isTest); @@ -151,7 +154,7 @@ private function getOriginId(Synchronization $synchronization, array $object): i $sourceConfig = $synchronization->getSourceConfig(); // Check if a custom ID position is defined in the source configuration - if (isset($sourceConfig['idPosition']) === true) { + if (isset($sourceConfig['idPosition']) === true && empty($sourceConfig['idPosition']) === false) { // Override default with custom ID position from config $originIdPosition = $sourceConfig['idPosition']; } @@ -381,7 +384,12 @@ public function getAllObjectsFromApi(Synchronization $synchronization, ?bool $is // Make the initial API call $response = $this->callService->call(source: $source, endpoint: $endpoint, method: 'GET', config: $config)->getResponse(); + $lastHash = md5($response['body']); $body = json_decode($response['body'], true); + if (empty($body) === true) { + // @todo log that we got a empty response + return []; + } $objects = array_merge($objects, $this->getAllObjectsFromArray(array: $body, synchronization: $synchronization)); // Return a single object or empty array if in test mode @@ -391,22 +399,31 @@ public function getAllObjectsFromApi(Synchronization $synchronization, ?bool $is // Current page is 2 because the first call made above is page 1. $currentPage = 2; - $usedNextEndpoint = false; + $useNextEndpoint = false; + if (array_key_exists('next', $body)) { + $useNextEndpoint = true; + } // Continue making API calls if there are more pages from 'next' the response body or if paginationQuery is set - while($nextEndpoint = $this->getNextEndpoint(body: $body, url: $source->getLocation(), sourceConfig: $sourceConfig, currentPage: $currentPage)) { - $usedNextEndpoint = true; + while($useNextEndpoint === true && $nextEndpoint = $this->getNextEndpoint(body: $body, url: $source->getLocation(), sourceConfig: $sourceConfig, currentPage: $currentPage)) { // Do not pass $config here becuase it overwrites the query attached to nextEndpoint $response = $this->callService->call(source: $source, endpoint: $nextEndpoint)->getResponse(); $body = json_decode($response['body'], true); $objects = array_merge($objects, $this->getAllObjectsFromArray($body, $synchronization)); } - if ($usedNextEndpoint === false) { + if ($useNextEndpoint === false) { do { $config = $this->getNextPage(config: $config, sourceConfig: $sourceConfig, currentPage: $currentPage); $response = $this->callService->call(source: $source, endpoint: $endpoint, method: 'GET', config: $config)->getResponse(); + $hash = md5($response['body']); + + if($hash === $lastHash) { + break; + } + + $lastHash = $hash; $body = json_decode($response['body'], true); if (empty($body) === true) { @@ -523,7 +540,6 @@ public function getAllObjectsFromArray(array $array, Synchronization $synchroniz */ public function getNextlinkFromCall(array $body): ?string { - // Check if the 'next' key exists in the response body return $body['next'] ?? null; } } diff --git a/lib/Twig/AuthenticationRuntime.php b/lib/Twig/AuthenticationRuntime.php index 34b59ac..1655229 100644 --- a/lib/Twig/AuthenticationRuntime.php +++ b/lib/Twig/AuthenticationRuntime.php @@ -2,9 +2,12 @@ namespace OCA\OpenConnector\Twig; +use Adbar\Dot; use GuzzleHttp\Exception\GuzzleException; use OCA\OpenConnector\Db\Source; use OCA\OpenConnector\Service\AuthenticationService; +use Symfony\Component\HttpFoundation\Exception\BadRequestException; +use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; use Twig\Extension\RuntimeExtensionInterface; class AuthenticationRuntime implements RuntimeExtensionInterface @@ -25,8 +28,12 @@ public function __construct( */ public function oauthToken(Source $source): string { + $configuration = new Dot($source->getConfiguration(), true); + + $authConfig = $configuration->get('authentication'); + return $this->authService->fetchOAuthTokens( - configuration: $source->getAuthenticationConfig() + configuration: $authConfig ); } @@ -39,8 +46,12 @@ public function oauthToken(Source $source): string */ public function jwtToken(Source $source): string { + $configuration = new Dot($source->getConfiguration(), true); + + $authConfig = $configuration->get('authentication'); + return $this->authService->fetchJWTToken( - configuration: $source->getAuthenticationConfig() + configuration: $authConfig ); } } diff --git a/src/components/CreateEndpointDialog.vue b/src/components/CreateEndpointDialog.vue index 0519ecb..e69de29 100644 --- a/src/components/CreateEndpointDialog.vue +++ b/src/components/CreateEndpointDialog.vue @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/CreateWebhookDialog.vue b/src/components/CreateWebhookDialog.vue index 0519ecb..e69de29 100644 --- a/src/components/CreateWebhookDialog.vue +++ b/src/components/CreateWebhookDialog.vue @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/EditEndpointDialog.vue b/src/components/EditEndpointDialog.vue index 0519ecb..e69de29 100644 --- a/src/components/EditEndpointDialog.vue +++ b/src/components/EditEndpointDialog.vue @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/EditWebhookDialog.vue b/src/components/EditWebhookDialog.vue index 0519ecb..e69de29 100644 --- a/src/components/EditWebhookDialog.vue +++ b/src/components/EditWebhookDialog.vue @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/entities/callLog/index.js b/src/entities/callLog/index.js index 7b97bc1..ec8b447 100644 --- a/src/entities/callLog/index.js +++ b/src/entities/callLog/index.js @@ -1,4 +1,3 @@ export * from './callLog.ts' export * from './callLog.types.ts' export * from './callLog.mock.ts' - diff --git a/src/entities/endpoint/index.js b/src/entities/endpoint/index.js index d9efff0..5689c47 100644 --- a/src/entities/endpoint/index.js +++ b/src/entities/endpoint/index.js @@ -1,4 +1,3 @@ export * from './endpoint.ts' export * from './endpoint.types.ts' export * from './endpoint.mock.ts' - diff --git a/src/entities/index.js b/src/entities/index.js index 9e9bf6e..99d426c 100644 --- a/src/entities/index.js +++ b/src/entities/index.js @@ -8,4 +8,4 @@ export * from './webhook/index.js' export * from './mapping/index.js' export * from './synchronization/index.js' export * from './source/index.js' -export * from './callLog/index.js' +export * from './callLog/index.js' diff --git a/src/entities/jobLog/index.js b/src/entities/jobLog/index.js index df9f468..2596894 100644 --- a/src/entities/jobLog/index.js +++ b/src/entities/jobLog/index.js @@ -1,4 +1,3 @@ export * from './jobLog.types.ts' export * from './jobLog.types.ts' export * from './jobLog.mock.ts' - diff --git a/src/entities/mapping/mapping.ts b/src/entities/mapping/mapping.ts index 984092c..0574e29 100644 --- a/src/entities/mapping/mapping.ts +++ b/src/entities/mapping/mapping.ts @@ -13,7 +13,7 @@ export class Mapping extends ReadonlyBaseClass implements TMapping { public readonly name: string public readonly description: string public readonly mapping: Record - public readonly unset: any[] + public readonly unset: string[] public readonly cast: Record public readonly passThrough: boolean public readonly dateCreated: string @@ -47,7 +47,7 @@ export class Mapping extends ReadonlyBaseClass implements TMapping { name: z.string().max(255), description: z.string(), mapping: z.record(z.any()), - unset: z.array(z.any()), + unset: z.array(z.string()), cast: z.record(z.any()), passThrough: z.boolean(), dateCreated: z.string().or(z.literal('')), diff --git a/src/entities/mapping/mapping.types.ts b/src/entities/mapping/mapping.types.ts index 92e69b5..5d7a9ed 100644 --- a/src/entities/mapping/mapping.types.ts +++ b/src/entities/mapping/mapping.types.ts @@ -7,7 +7,7 @@ export type TMapping = { name: string description: string mapping: Record - unset: any[] + unset: string[] cast: Record passThrough: boolean dateCreated: string diff --git a/src/entities/source/source.mock.ts b/src/entities/source/source.mock.ts index d4aec98..3be4fa9 100644 --- a/src/entities/source/source.mock.ts +++ b/src/entities/source/source.mock.ts @@ -4,6 +4,7 @@ import { TSource } from './source.types' export const mockSourceData = (): TSource[] => [ { id: '5137a1e5-b54d-43ad-abd1-4b5bff5fcd3f', + uuid: '5137a1e5-b54d-43ad-abd1-4b5bff5fcd3f', name: 'Test Source 1', description: 'A test source for demonstration', location: 'https://api.test1.com', @@ -43,6 +44,7 @@ export const mockSourceData = (): TSource[] => [ }, { id: '4c3edd34-a90d-4d2a-8894-adb5836ecde8', + uuid: '4c3edd34-a90d-4d2a-8894-adb5836ecde8', name: 'Test Source 2', description: 'Another test source', location: 'https://api.test2.com', diff --git a/src/entities/source/source.ts b/src/entities/source/source.ts index f842a0c..7846c2d 100644 --- a/src/entities/source/source.ts +++ b/src/entities/source/source.ts @@ -7,6 +7,7 @@ import ReadonlyBaseClass from '../ReadonlyBaseClass.js' export class Source extends ReadonlyBaseClass implements TSource { public readonly id: string + public readonly uuid: string public readonly name: string public readonly description: string public readonly reference: string @@ -47,6 +48,7 @@ export class Source extends ReadonlyBaseClass implements TSource { constructor(source: TSource) { const processedSource: TSource = { id: source.id || null, + uuid: source.uuid || '', name: source.name || '', description: source.description || '', reference: source.reference || '', @@ -91,6 +93,7 @@ export class Source extends ReadonlyBaseClass implements TSource { public validate(): SafeParseReturnType { const schema = z.object({ id: z.string().nullable(), + uuid: z.string(), name: z.string().max(255), description: z.string(), reference: z.string(), diff --git a/src/entities/source/source.types.ts b/src/entities/source/source.types.ts index 1e4abd8..a47f21e 100644 --- a/src/entities/source/source.types.ts +++ b/src/entities/source/source.types.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ export type TSource = { id: string + uuid: string name: string description: string reference: string diff --git a/src/entities/synchronization/synchronization.ts b/src/entities/synchronization/synchronization.ts index 700367e..c9bac36 100644 --- a/src/entities/synchronization/synchronization.ts +++ b/src/entities/synchronization/synchronization.ts @@ -12,7 +12,7 @@ export class Synchronization extends ReadonlyBaseClass implements TSynchronizati public sourceType: string public sourceHash: string public sourceTargetMapping: string - public sourceConfig: object + public sourceConfig: Record public sourceLastChanged: string public sourceLastChecked: string public sourceLastSynced: string @@ -20,7 +20,7 @@ export class Synchronization extends ReadonlyBaseClass implements TSynchronizati public targetType: string public targetHash: string public targetSourceMapping: string - public targetConfig: object + public targetConfig: Record public targetLastChanged: string public targetLastChecked: string public targetLastSynced: string @@ -64,7 +64,7 @@ export class Synchronization extends ReadonlyBaseClass implements TSynchronizati sourceType: z.string(), sourceHash: z.string(), sourceTargetMapping: z.string(), - sourceConfig: z.object({}), + sourceConfig: z.record(z.string(), z.string()), sourceLastChanged: z.string(), sourceLastChecked: z.string(), sourceLastSynced: z.string(), @@ -72,7 +72,7 @@ export class Synchronization extends ReadonlyBaseClass implements TSynchronizati targetType: z.string(), targetHash: z.string(), targetSourceMapping: z.string(), - targetConfig: z.object({}), + targetConfig: z.record(z.string(), z.string()), targetLastChanged: z.string(), targetLastChecked: z.string(), targetLastSynced: z.string(), diff --git a/src/entities/synchronization/synchronization.types.ts b/src/entities/synchronization/synchronization.types.ts index e2e59bf..7dcf47a 100644 --- a/src/entities/synchronization/synchronization.types.ts +++ b/src/entities/synchronization/synchronization.types.ts @@ -6,7 +6,7 @@ export type TSynchronization = { sourceType: string sourceHash: string sourceTargetMapping: string - sourceConfig: object + sourceConfig: Record sourceLastChanged: string sourceLastChecked: string sourceLastSynced: string @@ -14,7 +14,7 @@ export type TSynchronization = { targetType: string targetHash: string targetSourceMapping: string - targetConfig: object + targetConfig: Record targetLastChanged: string targetLastChecked: string targetLastSynced: string diff --git a/src/modals/Endpoint/DeleteEndpoint.vue b/src/modals/Endpoint/DeleteEndpoint.vue index 130e212..77691a5 100644 --- a/src/modals/Endpoint/DeleteEndpoint.vue +++ b/src/modals/Endpoint/DeleteEndpoint.vue @@ -8,7 +8,7 @@ import { endpointStore, navigationStore } from '../../store/store.js' size="normal" :can-close="false">

- Do you want to delete {{ endpointStore.endpointItem.name }}? This action cannot be undone. + Do you want to delete {{ endpointStore.endpointItem?.name }}? This action cannot be undone.

diff --git a/src/modals/JobArgument/DeleteJobArgument.vue b/src/modals/JobArgument/DeleteJobArgument.vue index 75b779f..b41001e 100644 --- a/src/modals/JobArgument/DeleteJobArgument.vue +++ b/src/modals/JobArgument/DeleteJobArgument.vue @@ -22,7 +22,7 @@ import { navigationStore, jobStore } from '../../store/store.js' Do you want to delete {{ jobStore.jobArgumentKey }}? This action cannot be undone.

@@ -60,6 +68,8 @@ import EditJobArgument from './JobArgument/EditJobArgument.vue' import DeleteJobArgument from './JobArgument/DeleteJobArgument.vue' import EditSourceConfiguration from './SourceConfiguration/EditSourceConfiguration.vue' import DeleteSourceConfiguration from './SourceConfiguration/DeleteSourceConfiguration.vue' +import EditSourceConfigurationAuthentication from './SourceConfigurationAuthentication/EditSourceConfigurationAuthentication.vue' +import DeleteSourceConfigurationAuthentication from './SourceConfigurationAuthentication/DeleteSourceConfigurationAuthentication.vue' import ViewSourceLog from './Log/ViewSourceLog.vue' import EditMappingMapping from './mappingMapping/EditMappingMapping.vue' import DeleteMappingMapping from './mappingMapping/DeleteMappingMapping.vue' @@ -68,6 +78,12 @@ import DeleteMappingCast from './mappingCast/DeleteMappingCast.vue' import ViewJobLog from './Log/ViewJobLog.vue' import ViewSynchronizationLog from './Log/ViewSynchronizationLog.vue' import ViewSynchronizationContract from './Log/ViewSynchronizationContract.vue' +import EditMappingUnset from './mappingUnset/EditMappingUnset.vue' +import DeleteMappingUnset from './mappingUnset/DeleteMappingUnset.vue' +import EditSynchronizationSourceConfig from './SynchronizationSourceConfig/EditSynchronizationSourceConfig.vue' +import DeleteSynchronizationSourceConfig from './SynchronizationSourceConfig/DeleteSynchronizationSourceConfig.vue' +import EditSynchronizationTargetConfig from './SynchronizationTargetConfig/EditSynchronizationTargetConfig.vue' +import DeleteSynchronizationTargetConfig from './SynchronizationTargetConfig/DeleteSynchronizationTargetConfig.vue' export default { name: 'Modals', @@ -94,6 +110,8 @@ export default { DeleteJobArgument, EditSourceConfiguration, DeleteSourceConfiguration, + EditSourceConfigurationAuthentication, + DeleteSourceConfigurationAuthentication, ViewSourceLog, EditMappingMapping, DeleteMappingMapping, @@ -102,6 +120,12 @@ export default { ViewJobLog, ViewSynchronizationLog, ViewSynchronizationContract, + EditMappingUnset, + DeleteMappingUnset, + EditSynchronizationSourceConfig, + DeleteSynchronizationSourceConfig, + EditSynchronizationTargetConfig, + DeleteSynchronizationTargetConfig, }, setup() { return { diff --git a/src/modals/SourceConfigurationAuthentication/DeleteSourceConfigurationAuthentication.vue b/src/modals/SourceConfigurationAuthentication/DeleteSourceConfigurationAuthentication.vue new file mode 100644 index 0000000..f319319 --- /dev/null +++ b/src/modals/SourceConfigurationAuthentication/DeleteSourceConfigurationAuthentication.vue @@ -0,0 +1,118 @@ + + + + + + + diff --git a/src/modals/SourceConfigurationAuthentication/EditSourceConfigurationAuthentication.vue b/src/modals/SourceConfigurationAuthentication/EditSourceConfigurationAuthentication.vue new file mode 100644 index 0000000..b12a1e1 --- /dev/null +++ b/src/modals/SourceConfigurationAuthentication/EditSourceConfigurationAuthentication.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/src/modals/Synchronization/EditSynchronization.vue b/src/modals/Synchronization/EditSynchronization.vue index bf1aa3e..1a76f1b 100644 --- a/src/modals/Synchronization/EditSynchronization.vue +++ b/src/modals/Synchronization/EditSynchronization.vue @@ -1,22 +1,78 @@