diff --git a/CHANGELOG.md b/CHANGELOG.md index 47f026cbe06..a050d37a0d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ HTTP input now accepts `field('..')` references in the HTTP body definition. +## 1.9.2 + +### New features + +- Toolkit now exports `window().location.get` to country config that can be used as a template variable e.g. in HttpField request body. + ## 1.9.1 ### Breaking changes diff --git a/license-config.json b/license-config.json index d83c92b37d6..8cd49d2728e 100644 --- a/license-config.json +++ b/license-config.json @@ -55,6 +55,7 @@ "packages/events/.kanelrc.js", "packages/dashboards/data/metabase/*", "packages/events/src/tests/extract-dump.sh", + "packages/toolkit/build.sh", ".kube" ], "license": "license-header.txt", diff --git a/package.json b/package.json index be12c8fcde0..ece4e338199 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "description": "OpenCRVS core workspace", "license": "MPL-2.0", - "version": "1.9.0", + "version": "1.9.2", "private": true, "os": [ "darwin", diff --git a/packages/api-docs/package.json b/packages/api-docs/package.json index 041b67f51fc..13cc59835f7 100644 --- a/packages/api-docs/package.json +++ b/packages/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/api-docs", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS API documentation", "license": "MPL-2.0", "scripts": { diff --git a/packages/auth/package.json b/packages/auth/package.json index 2a8037f191b..02440659c4e 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/auth", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS authentication service", "license": "MPL-2.0", "private": true, diff --git a/packages/auth/src/features/authenticate/service.ts b/packages/auth/src/features/authenticate/service.ts index 1d1cd0187a0..a51cb3a17a7 100644 --- a/packages/auth/src/features/authenticate/service.ts +++ b/packages/auth/src/features/authenticate/service.ts @@ -178,6 +178,41 @@ export async function createTokenForActionConfirmation( ) } +export async function createTokenForVerifiableCredentialIssuance( + eventId: UUID, + userId: string +) { + return sign( + { + scope: ['record.issue-vc'], + eventId, + userType: TokenUserType.enum.user + }, + cert, + { + subject: userId, + algorithm: 'RS256', + expiresIn: '5 minutes', + audience: [ + 'opencrvs:gateway-user', + 'opencrvs:user-mgnt-user', + 'opencrvs:auth-user', + 'opencrvs:hearth-user', + 'opencrvs:notification-user', + 'opencrvs:workflow-user', + 'opencrvs:search-user', + 'opencrvs:metrics-user', + 'opencrvs:countryconfig-user', + 'opencrvs:webhooks-user', + 'opencrvs:config-user', + 'opencrvs:documents-user', + 'opencrvs:notification-api-user' + ], + issuer: JWT_ISSUER + } + ) +} + export async function storeUserInformation( nonce: string, userFullName: IUserName[], diff --git a/packages/auth/src/features/oauthToken/handler.ts b/packages/auth/src/features/oauthToken/handler.ts index 787f97708b5..a174af52944 100644 --- a/packages/auth/src/features/oauthToken/handler.ts +++ b/packages/auth/src/features/oauthToken/handler.ts @@ -12,6 +12,12 @@ import * as Hapi from '@hapi/hapi' import { clientCredentialsHandler } from './client-credentials' import * as oauthResponse from './responses' import { tokenExchangeHandler } from './token-exchange' +import { preAuthorizedCodeHandler } from './pre-authorized-code' + +const CLIENT_CREDENTIALS = 'client_credentials' +const TOKEN_EXCHANGE = 'urn:opencrvs:oauth:grant-type:token-exchange' +const PRE_AUTHORIZED_CODE_GRANT = + 'urn:ietf:params:oauth:grant-type:pre-authorized_code' export async function tokenHandler( request: Hapi.Request, @@ -19,13 +25,17 @@ export async function tokenHandler( ) { const grantType = request.query.grant_type - if (grantType === 'client_credentials') { + if (grantType === CLIENT_CREDENTIALS) { return clientCredentialsHandler(request, h) } - if (grantType === 'urn:opencrvs:oauth:grant-type:token-exchange') { + if (grantType === TOKEN_EXCHANGE) { return tokenExchangeHandler(request, h) } + if (grantType === PRE_AUTHORIZED_CODE_GRANT) { + return preAuthorizedCodeHandler(request, h) + } + return oauthResponse.invalidGrantType(h) } diff --git a/packages/auth/src/features/oauthToken/pre-authorized-code.ts b/packages/auth/src/features/oauthToken/pre-authorized-code.ts new file mode 100644 index 00000000000..047040b5ae3 --- /dev/null +++ b/packages/auth/src/features/oauthToken/pre-authorized-code.ts @@ -0,0 +1,45 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ + +import * as Hapi from '@hapi/hapi' +import * as oauthResponse from './responses' +import { createTokenForVerifiableCredentialIssuance } from '../authenticate/service' + +/** + * Issues a token for a pre-authorized code grant. + * This is used in OID4VC flows where a client has obtained a pre-authorized code to get the verifiable credential. + */ +export async function preAuthorizedCodeHandler( + request: Hapi.Request, + h: Hapi.ResponseToolkit +) { + const preAuthorizedCode = request.query?.['pre-authorized_code'] // for PoC: this is event ID + + if (!preAuthorizedCode) { + return oauthResponse.invalidRequest(h, 'missing pre-authorized_code') + } + + // const token = request.headers.authorization.replace('Bearer ', '') + // const decodedOrError = pipe(token, verifyToken) + + // if (decodedOrError._tag === 'Left') { + // return oauthResponse.invalidSubjectToken(h) + // } + + // const { sub } = decodedOrError.right + + const token = await createTokenForVerifiableCredentialIssuance( + preAuthorizedCode, + 'anonymous-user' // In PoC, no user binding, see above comment when needed + ) + + return oauthResponse.preAuthorizedCodeSuccess(h, token) +} diff --git a/packages/auth/src/features/oauthToken/responses.ts b/packages/auth/src/features/oauthToken/responses.ts index 7fac3f7f346..cc3fad31d59 100644 --- a/packages/auth/src/features/oauthToken/responses.ts +++ b/packages/auth/src/features/oauthToken/responses.ts @@ -10,12 +10,14 @@ */ import * as Hapi from '@hapi/hapi' -export const invalidRequest = (h: Hapi.ResponseToolkit) => +export const invalidRequest = ( + h: Hapi.ResponseToolkit, + description = 'Invalid request. Check that all required parameters have been provided.' +) => h .response({ error: 'invalid_request', - error_description: - 'Invalid request. Check that all required parameters have been provided.', + error_description: description, error_uri: 'Refer to https://documentation.opencrvs.org/technology/interoperability/authenticate-a-client' }) @@ -62,3 +64,16 @@ export const success = (h: Hapi.ResponseToolkit, token: string) => }) .header('Cache-Control', 'no-store') .code(200) + +export const preAuthorizedCodeSuccess = ( + h: Hapi.ResponseToolkit, + token: string +) => + h + .response({ + access_token: token, + token_type: 'Bearer', + expires_in: 300 // 5 minutes + }) + .header('Cache-Control', 'no-store') + .code(200) diff --git a/packages/auth/src/features/openid4vc/well-known-openid-credentials.ts b/packages/auth/src/features/openid4vc/well-known-openid-credentials.ts new file mode 100644 index 00000000000..12636b7d250 --- /dev/null +++ b/packages/auth/src/features/openid4vc/well-known-openid-credentials.ts @@ -0,0 +1,38 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ + +import { env } from '@auth/environment' +import * as Hapi from '@hapi/hapi' + +// @TODO: What is actually the best way to figure this out? +// - [ ] should we pass a new environment variable? +// - [ ] should this live not in auth? +const GATEWAY = env.isProduction + ? `https://gateway.${env.DOMAIN}` + : 'http://localhost:7070' + +export function openidCredentialIssuerHandler( + request: Hapi.Request, + h: Hapi.ResponseToolkit +) { + return h.response({ + credential_issuer: GATEWAY, + credential_endpoint: `${GATEWAY}/events/openid4vc.issuance.credential`, + token_endpoint: `${GATEWAY}/auth/token`, + + credential_configurations_supported: { + BirthCertificateCredential: { + format: 'jwt_vc_json' + // claim schema etc... + } + } + }) +} diff --git a/packages/auth/src/server.ts b/packages/auth/src/server.ts index 9e7903c19eb..2cf46e18ec5 100644 --- a/packages/auth/src/server.ts +++ b/packages/auth/src/server.ts @@ -69,6 +69,7 @@ import reindexingTokenHandler, { responseSchema as reindexResponseSchema } from './features/reindexToken/handler' import { Boom, badRequest } from '@hapi/boom' +import { openidCredentialIssuerHandler } from './features/openid4vc/well-known-openid-credentials' export type AuthServer = { server: Hapi.Server @@ -128,7 +129,15 @@ export async function createServer() { server.route({ method: 'GET', path: '/.well-known', - handler: getPublicKey, + handler: (_, h) => h.response(getPublicKey()).type('text/plain'), + options: { + tags: ['api'] + } + }) + server.route({ + method: 'GET', + path: '/.well-known/openid-credential-issuer', + handler: openidCredentialIssuerHandler, options: { tags: ['api'] } diff --git a/packages/client/package.json b/packages/client/package.json index ec2afe7aa90..39f58b86f63 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/client", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS client application", "license": "MPL-2.0", "private": true, diff --git a/packages/client/src/hooks/useHomePage.ts b/packages/client/src/hooks/useHomePage.ts index da0e459191a..d8152251a4c 100644 --- a/packages/client/src/hooks/useHomePage.ts +++ b/packages/client/src/hooks/useHomePage.ts @@ -32,10 +32,23 @@ export const useHomePage = () => { const { pathname } = useLocation() const { routes } = useNavigation() const userDetails = useSelector(getUserDetails) - const firstNavItem = routes - .filter((navigationGroup) => navigationGroup.tabs.length > 0) - .at(0) - ?.tabs.at(0)?.name + + const firstDashboard = window.config.DASHBOARDS?.at(0)?.id + + const tabs = routes.flatMap((navigationGroup) => navigationGroup.tabs) + + const firstNavItem = tabs + .filter((tab) => { + if ( + tab.name === WORKQUEUE_TABS.dashboard && + window.config.DASHBOARDS.length === 0 + ) { + // ignores dashboard tab when no dashboard is configured + return false + } + return true + }) + .at(0)?.name let path: string | Partial = REGISTRAR_HOME @@ -76,7 +89,7 @@ export const useHomePage = () => { path = ALL_USER_EMAIL break case WORKQUEUE_TABS.dashboard: - path = DASHBOARD + path = formatUrl(DASHBOARD, { id: firstDashboard! }) break case WORKQUEUE_TABS.performance: path = generatePerformanceHomeUrl({ diff --git a/packages/client/src/v2-events/components/forms/FormFieldGenerator/FormFieldGenerator-2.interaction.stories.tsx b/packages/client/src/v2-events/components/forms/FormFieldGenerator/FormFieldGenerator-2.interaction.stories.tsx index 8a005225cab..4e569d75025 100644 --- a/packages/client/src/v2-events/components/forms/FormFieldGenerator/FormFieldGenerator-2.interaction.stories.tsx +++ b/packages/client/src/v2-events/components/forms/FormFieldGenerator/FormFieldGenerator-2.interaction.stories.tsx @@ -505,7 +505,7 @@ export const DisabledFormFields: StoryObj = { // in react-select v5 literal inputs are not rendered when disabled, so we ensure the fields are there. await canvas.findByLabelText('Region') - await canvas.findByLabelText('District *') + await canvas.findByLabelText('District') }) } } @@ -559,7 +559,7 @@ export const EnabledFormFields: StoryObj = { } await canvas.findByLabelText('Region') - await canvas.findByLabelText('District *') + await canvas.findByLabelText('District') }) } } @@ -647,9 +647,7 @@ export const EnabledFormFieldsByEnableCondition: StoryObj< } await expect(await canvas.findByLabelText('Region')).not.toBeDisabled() - await expect( - await canvas.findByLabelText('District *') - ).not.toBeDisabled() + await expect(await canvas.findByLabelText('District')).not.toBeDisabled() }) await step('All form fields should be disabled again', async () => { @@ -674,7 +672,7 @@ export const EnabledFormFieldsByEnableCondition: StoryObj< // in react-select v5 literal inputs are not rendered when disabled, so we ensure the fields are there. await canvas.findByLabelText('Region') - await canvas.findByLabelText('District *') + await canvas.findByLabelText('District') } }) } diff --git a/packages/client/src/v2-events/components/forms/FormFieldGenerator/GeneratedInputField.tsx b/packages/client/src/v2-events/components/forms/FormFieldGenerator/GeneratedInputField.tsx index 16dd6d771cc..8fe79130eee 100644 --- a/packages/client/src/v2-events/components/forms/FormFieldGenerator/GeneratedInputField.tsx +++ b/packages/client/src/v2-events/components/forms/FormFieldGenerator/GeneratedInputField.tsx @@ -62,7 +62,8 @@ import { isIdReaderFieldType, isQrReaderFieldType, isLoaderFieldType, - isAgeFieldType + isAgeFieldType, + isImageFieldType } from '@opencrvs/commons/client' import { TextArea } from '@opencrvs/components/lib/TextArea' import { InputField } from '@client/components/form/InputField' @@ -87,7 +88,8 @@ import { AlphaPrintButton, Http, LinkButton, - VerificationStatus + VerificationStatus, + Image } from '@client/v2-events/features/events/registered-fields' import { Address } from '@client/v2-events/features/events/registered-fields/Address' import { Data } from '@client/v2-events/features/events/registered-fields/Data' @@ -800,6 +802,17 @@ export const GeneratedInputField = React.memo( ) } + if (isImageFieldType(field)) { + return ( + field.value && ( + + ) + ) + } + throw new Error(`Unsupported field ${JSON.stringify(fieldDefinition)}`) } ) diff --git a/packages/client/src/v2-events/features/events/Search/utils.ts b/packages/client/src/v2-events/features/events/Search/utils.ts index 7dd45bdd00e..20ee5ee87a2 100644 --- a/packages/client/src/v2-events/features/events/Search/utils.ts +++ b/packages/client/src/v2-events/features/events/Search/utils.ts @@ -420,12 +420,20 @@ function applySearchFieldOverridesToFieldConfig( } } if (field.type === FieldType.ADDRESS) { + const streetAddressForm = field.configuration?.streetAddressForm?.map( + (subField) => ({ + ...subField, + required: false + }) + ) + return { ...field, ...commonConfig, configuration: { ...field.configuration, - fields: ['country'] + fields: ['country'], + streetAddressForm } } } diff --git a/packages/client/src/v2-events/features/events/actions/print-certificate/pdfUtils.test.ts b/packages/client/src/v2-events/features/events/actions/print-certificate/pdfUtils.test.ts index 51583d47f27..aaeff82c420 100644 --- a/packages/client/src/v2-events/features/events/actions/print-certificate/pdfUtils.test.ts +++ b/packages/client/src/v2-events/features/events/actions/print-certificate/pdfUtils.test.ts @@ -11,7 +11,7 @@ import { createIntl } from 'react-intl' import createFetchMock from 'vitest-fetch-mock' -import { ContentSvg } from 'pdfmake/interfaces' +import { Content, ContentSvg } from 'pdfmake/interfaces' import { ActionDocument, eventQueryDataGenerator, @@ -163,6 +163,36 @@ describe('svgToPdfTemplate', () => { `.trim() ) }) + + test('multipage certificate', async () => { + const svgString = ` + + + + + + + + + `.trim() + + const result = await svgToPdfTemplate(svgString, {}) + const contents = result.definition.content as ContentSvg[] + + expect(contents.length).toBe(2) + + expect(contents[0].svg).toContain( + '\n' + + ' \n' + + ' ' + ) + expect(contents[1].svg).toContain( + '\n' + + ' \n' + + ' ' + ) + expect(result.definition.pageSize).toEqual({ width: 200, height: 200 }) + }) }) function expectRenderOutput(template: string, output: string) { diff --git a/packages/client/src/v2-events/features/events/actions/print-certificate/pdfUtils.ts b/packages/client/src/v2-events/features/events/actions/print-certificate/pdfUtils.ts index 1e0dd73c66f..608b3acb7b2 100644 --- a/packages/client/src/v2-events/features/events/actions/print-certificate/pdfUtils.ts +++ b/packages/client/src/v2-events/features/events/actions/print-certificate/pdfUtils.ts @@ -681,12 +681,15 @@ export async function svgToPdfTemplate( 'image/svg+xml' ).documentElement + const $sections = svgElement.querySelectorAll('[data-page]') const widthValue = svgElement.getAttribute('width') const heightValue = svgElement.getAttribute('height') if (widthValue && heightValue) { const width = Number.parseInt(widthValue) - const height = Number.parseInt(heightValue) + const height = $sections.length + ? Number.parseInt(heightValue) / $sections.length + : Number.parseInt(heightValue) pdfTemplate.definition.pageSize = { width, height @@ -717,12 +720,27 @@ export async function svgToPdfTemplate( } as Content) } - pdfTemplate.definition.content = [ - { - svg: svgWithInlineImages - }, - ...absolutelyPositionedHTMLs - ] + if ($sections.length > 0) { + pdfTemplate.definition.content = [ + ...Array.from($sections).map(($section) => { + const $svgWrapper = document.createElement('svg') + ;[...svgElement.attributes].forEach((attr) => { + $svgWrapper.setAttribute(attr.name, attr.value) + }) + $section.removeAttribute('transform') + $svgWrapper.appendChild($section.cloneNode(true)) + return { svg: $svgWrapper.outerHTML } + }), + ...absolutelyPositionedHTMLs + ] + } else { + pdfTemplate.definition.content = [ + { + svg: svgWithInlineImages + }, + ...absolutelyPositionedHTMLs + ] + } return pdfTemplate } diff --git a/packages/client/src/v2-events/features/events/registered-fields/Address.tsx b/packages/client/src/v2-events/features/events/registered-fields/Address.tsx index c50244ecd1c..284e0085159 100644 --- a/packages/client/src/v2-events/features/events/registered-fields/Address.tsx +++ b/packages/client/src/v2-events/features/events/registered-fields/Address.tsx @@ -32,7 +32,8 @@ import { LocationType, ValidatorContext, DomesticAddressFieldValue, - UUID + UUID, + RequireConfig } from '@opencrvs/commons/client' import { FormFieldGenerator } from '@client/v2-events/components/forms/FormFieldGenerator' import { Output } from '@client/v2-events/features/events/components/Output' @@ -134,7 +135,8 @@ function isDomesticAddress() { } function generateAdminStructureFields( - inputArray: AdminStructureItem[] + inputArray: AdminStructureItem[], + required: RequireConfig ): AdministrativeArea[] { return inputArray.map((item, index) => { const { id, label } = item @@ -168,7 +170,7 @@ function generateAdminStructureFields( type: FieldType.ADMINISTRATIVE_AREA, conditionals, parent: createFieldCondition(parentId), - required: true, + required, label, configuration } @@ -242,7 +244,10 @@ function AddressInput(props: Props) { const userDetails = useSelector(getUserDetails) const appConfigAdminLevels = config.ADMIN_STRUCTURE const adminLevelIds = appConfigAdminLevels.map((level) => level.id) - const adminStructure = generateAdminStructureFields(appConfigAdminLevels) + const adminStructure = generateAdminStructureFields( + appConfigAdminLevels, + otherProps.required + ) const customAddressFields = props.configuration?.streetAddressForm const adminStructureLocations = new Map( @@ -306,23 +311,25 @@ function AddressInput(props: Props) { adminLevelIds ) - const fields = [COUNTRY_FIELD, ...adminStructure, ...addressFields].map( - (x) => { - const existingEnableCondition = - x.conditionals?.find((c) => c.type === ConditionalType.ENABLE) - ?.conditional ?? not(not(alwaysTrue())) - return { - ...x, - conditionals: [ - ...(x.conditionals ?? []), - { - type: ConditionalType.ENABLE, - conditional: disabled ? not(alwaysTrue()) : existingEnableCondition - } - ] - } + const fields = [ + { ...COUNTRY_FIELD, required: otherProps.required }, + ...adminStructure, + ...addressFields + ].map((x) => { + const existingEnableCondition = + x.conditionals?.find((c) => c.type === ConditionalType.ENABLE) + ?.conditional ?? not(not(alwaysTrue())) + return { + ...x, + conditionals: [ + ...(x.conditionals ?? []), + { + type: ConditionalType.ENABLE, + conditional: disabled ? not(alwaysTrue()) : existingEnableCondition + } + ] } - ) + }) const handleChange = (values: EventState) => { const addressLines = extractAddressLines(values, adminLevelIds) @@ -402,7 +409,10 @@ function AddressOutput({ ...adminLevels } - const adminStructure = generateAdminStructureFields(appConfigAdminLevels) + const adminStructure = generateAdminStructureFields( + appConfigAdminLevels, + configuration?.required + ) const addressFields = Array.isArray(customAddressFields) && customAddressFields.length > 0 @@ -423,7 +433,11 @@ function AddressOutput({ const flattenedAddressValues = flattenAddressObject(addressValues) - const fieldsToShow = [COUNTRY_FIELD, ...adminStructure, ...addressFields] + const fieldsToShow = [ + { ...COUNTRY_FIELD, required: configuration?.required }, + ...adminStructure, + ...addressFields + ] .map((field) => ({ field, value: flattenedAddressValues[field.id] diff --git a/packages/client/src/v2-events/features/events/registered-fields/Http.tsx b/packages/client/src/v2-events/features/events/registered-fields/Http.tsx index 8cdf55f9931..0e6ee199751 100644 --- a/packages/client/src/v2-events/features/events/registered-fields/Http.tsx +++ b/packages/client/src/v2-events/features/events/registered-fields/Http.tsx @@ -71,12 +71,11 @@ async function fetchHttpFieldValue( for (const [k, v] of Object.entries(cfg.body)) { if (isTemplateVariable(v)) { cfg.body[k] = getMixedPath(systemVariables, v) - } - - if (isFieldReference(v)) { + } else if (isFieldReference(v)) { cfg.body[k] = parseFieldReferenceToValue(v, form) + } else { + cfg.body[k] = v } - cfg.body[k] = v } } diff --git a/packages/client/src/v2-events/features/events/registered-fields/Image.tsx b/packages/client/src/v2-events/features/events/registered-fields/Image.tsx new file mode 100644 index 00000000000..af75eb7f401 --- /dev/null +++ b/packages/client/src/v2-events/features/events/registered-fields/Image.tsx @@ -0,0 +1,28 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ +import * as React from 'react' +import { useIntl } from 'react-intl' +import { TranslationConfig } from '@opencrvs/commons/client' + +interface ImageInputProps { + value: string + alt: TranslationConfig +} + +function ImageOutput({ value, alt }: ImageInputProps) { + const intl = useIntl() + return {intl.formatMessage(alt)} +} + +export const Image = { + Input: ImageOutput, + Output: ImageOutput +} diff --git a/packages/client/src/v2-events/features/events/registered-fields/index.ts b/packages/client/src/v2-events/features/events/registered-fields/index.ts index b2bd7cc3290..a14a3a81776 100644 --- a/packages/client/src/v2-events/features/events/registered-fields/index.ts +++ b/packages/client/src/v2-events/features/events/registered-fields/index.ts @@ -36,6 +36,7 @@ import { AlphaPrintButton } from './AlphaPrintButton' import { LinkButton } from './LinkButton' import { VerificationStatus } from './VerificationStatus' import { QueryParamReader } from './QueryParamReader' +import { Image } from './Image' export * from './Address' export * from './AdministrativeArea' @@ -60,6 +61,7 @@ export * from './TimeField' export * from './AlphaPrintButton' export * from './LinkButton' export * from './VerificationStatus' +export * from './Image' export function getRegisteredFieldByFieldConfig( type: T @@ -119,6 +121,8 @@ export function getRegisteredFieldByFieldConfig( return QueryParamReader case FieldType.AGE: return AgeField + case FieldType.IMAGE: + return Image default: return undefined } diff --git a/packages/client/src/v2-events/features/workqueues/EventOverview/EventOverview.stories.tsx b/packages/client/src/v2-events/features/workqueues/EventOverview/EventOverview.stories.tsx index 445b710e7c6..efed6febcb6 100644 --- a/packages/client/src/v2-events/features/workqueues/EventOverview/EventOverview.stories.tsx +++ b/packages/client/src/v2-events/features/workqueues/EventOverview/EventOverview.stories.tsx @@ -661,7 +661,12 @@ const duplicateEvent = { generateActionDocument({ configuration: tennisClubMembershipEvent, action: ActionType.DUPLICATE_DETECTED, - defaults: actionDefaults + defaults: { + ...actionDefaults, + content: { + duplicates: [{ id: getUUID(), trackingId: '0R1G1NAL' }] + } + } }), generateActionDocument({ configuration: tennisClubMembershipEvent, diff --git a/packages/client/src/v2-events/features/workqueues/EventOverview/components/EventHistory/EventHistoryDialog/components/ActionTypeSpecificContent.tsx b/packages/client/src/v2-events/features/workqueues/EventOverview/components/EventHistory/EventHistoryDialog/components/ActionTypeSpecificContent.tsx index ac2433909ab..98a665b9e9c 100644 --- a/packages/client/src/v2-events/features/workqueues/EventOverview/components/EventHistory/EventHistoryDialog/components/ActionTypeSpecificContent.tsx +++ b/packages/client/src/v2-events/features/workqueues/EventOverview/components/EventHistory/EventHistoryDialog/components/ActionTypeSpecificContent.tsx @@ -23,6 +23,7 @@ import { RequestCorrection } from './RequestCorrection' import { PrintCertificate } from './PrintCertificate' import { DeclarationUpdate } from './DeclarationUpdate' import { CustomActionContent } from './CustomAction' +import { DetectedDuplicate } from './DetectedDuplicate' const SyntheticDeclarationActionTypes = z.enum([DECLARATION_ACTION_UPDATE]) @@ -71,6 +72,10 @@ export function ActionTypeSpecificContent({ ) } + if (type === ActionType.DUPLICATE_DETECTED) { + return + } + if (type === ActionType.CUSTOM) { return ( ({ + matchedTo: trackingId + })) + + return ( +
+ ) +} diff --git a/packages/client/src/v2-events/hooks/useSystemVariables.ts b/packages/client/src/v2-events/hooks/useSystemVariables.ts index dabf06d539e..0b708660944 100644 --- a/packages/client/src/v2-events/hooks/useSystemVariables.ts +++ b/packages/client/src/v2-events/hooks/useSystemVariables.ts @@ -9,6 +9,7 @@ * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ +import { pick } from 'lodash' import { SystemVariables } from '@opencrvs/commons/client' import { useUserDetails } from './useUserDetails' @@ -19,7 +20,10 @@ export function useSystemVariables() { const user = useUserDetails() const variables = { - $user: user + $user: user, + $window: { + location: pick(window.location, ['href', 'pathname', 'hostname']) + } } satisfies SystemVariables return variables diff --git a/packages/client/src/v2-events/utils.test.ts b/packages/client/src/v2-events/utils.test.ts index fa8a7ddc3e5..d57b34e30b7 100644 --- a/packages/client/src/v2-events/utils.test.ts +++ b/packages/client/src/v2-events/utils.test.ts @@ -198,6 +198,13 @@ const testCases = [ $user: { district: '', province: '' + }, + $window: { + location: { + href: 'http://example.com', + pathname: '/path', + hostname: 'example.com' + } } }, expected: undefined, @@ -210,6 +217,13 @@ const testCases = [ $user: { district: '', province: '' + }, + $window: { + location: { + href: 'http://example.com', + pathname: '/path', + hostname: 'example.com' + } } }, expected: 'Hello', @@ -222,6 +236,13 @@ const testCases = [ $user: { district: 'Ibombo', province: '' + }, + $window: { + location: { + href: 'http://example.com', + pathname: '/path', + hostname: 'example.com' + } } }, expected: 'Ibombo', @@ -234,6 +255,13 @@ const testCases = [ $user: { district: 'Ibombo', province: '' + }, + $window: { + location: { + href: 'http://example.com', + pathname: '/path', + hostname: 'example.com' + } } }, expected: 'Hello world', @@ -249,6 +277,13 @@ const testCases = [ $user: { district: 'Ibombo', province: 'Central' + }, + $window: { + location: { + href: 'http://example.com', + pathname: '/path', + hostname: 'example.com' + } } }, expected: { diff --git a/packages/commons/package.json b/packages/commons/package.json index 36787547d45..5ee58ea78a1 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/commons", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS common modules and utils", "license": "MPL-2.0", "main": "./build/dist/common/index.js", diff --git a/packages/commons/src/events/FieldConfig.ts b/packages/commons/src/events/FieldConfig.ts index 2bdf3ef85a4..9ad00c55a3c 100644 --- a/packages/commons/src/events/FieldConfig.ts +++ b/packages/commons/src/events/FieldConfig.ts @@ -88,6 +88,8 @@ const requiredSchema = z .default(false) .optional() +export type RequireConfig = z.infer + const BaseField = z .object({ id: FieldId.describe('Unique identifier of the field.'), @@ -844,6 +846,18 @@ const LoaderField = BaseField.extend({ export type LoaderField = z.infer +const ImageField = BaseField.extend({ + type: z.literal(FieldType.IMAGE), + defaultValue: z.string().optional().describe('Source URL of the image'), + configuration: z.object({ + alt: TranslationConfig.describe('Alternative text for the image') + }) +}).describe( + 'A non-interactive field that displays an image in the form such as a QR code or logo' +) + +export type ImageField = z.infer + export const FieldConfig = z .discriminatedUnion('type', [ Address, @@ -884,7 +898,8 @@ export const FieldConfig = z IdReaderField, QueryParamReaderField, LoaderField, - SearchField + SearchField, + ImageField ]) .meta({ description: 'Form field configuration', diff --git a/packages/commons/src/events/FieldType.ts b/packages/commons/src/events/FieldType.ts index d289139b972..b5f60d9a6b8 100644 --- a/packages/commons/src/events/FieldType.ts +++ b/packages/commons/src/events/FieldType.ts @@ -50,7 +50,8 @@ export const FieldType = { QUERY_PARAM_READER: 'QUERY_PARAM_READER', QR_READER: 'QR_READER', ID_READER: 'ID_READER', - LOADER: 'LOADER' + LOADER: 'LOADER', + IMAGE: 'IMAGE' } as const /** diff --git a/packages/commons/src/events/FieldTypeMapping.ts b/packages/commons/src/events/FieldTypeMapping.ts index bc534c46ecd..63b2fb29afa 100644 --- a/packages/commons/src/events/FieldTypeMapping.ts +++ b/packages/commons/src/events/FieldTypeMapping.ts @@ -50,7 +50,8 @@ import { QrReaderField, IdReaderField, LoaderField, - AgeField + AgeField, + ImageField } from './FieldConfig' import { FieldType } from './FieldType' import { @@ -159,6 +160,7 @@ export function mapFieldTypeToZod(field: FieldConfig, actionType?: ActionType) { case FieldType.VERIFICATION_STATUS: case FieldType.ID: case FieldType.LOADER: + case FieldType.IMAGE: schema = field.required ? NonEmptyTextValue : TextValue break case FieldType.NUMBER: @@ -250,6 +252,7 @@ export function mapFieldTypeToEmptyValue(field: FieldConfig) { case FieldType.QR_READER: case FieldType.ID_READER: case FieldType.LOADER: + case FieldType.IMAGE: return null case FieldType.ADDRESS: return { @@ -555,6 +558,13 @@ export const isLoaderFieldType = (field: { return field.config.type === FieldType.LOADER } +export const isImageFieldType = (field: { + config: FieldConfig + value: FieldValue | FieldUpdateValue +}): field is { value: string | undefined; config: ImageField } => { + return field.config.type === FieldType.IMAGE +} + export type NonInteractiveFieldType = | Divider | PageHeader diff --git a/packages/commons/src/events/TemplateConfig.ts b/packages/commons/src/events/TemplateConfig.ts index 1eff1bbc0b7..98bf5f43507 100644 --- a/packages/commons/src/events/TemplateConfig.ts +++ b/packages/commons/src/events/TemplateConfig.ts @@ -24,8 +24,26 @@ export type SystemVariables = { province: string district: string } + $window: { + location: { + href: string + pathname: string + hostname: string + } + } } +/** + * Resolves `window().location.get('href')` to `window.location.href` to allow us to 1) type check system variables 2) change the implementation later if needed + */ +export const window = () => ({ + location: { + get: (key: 'href' | 'pathname' | 'hostname') => { + return `$window.location.${key}` + } + } +}) + /** * Recursively flatten the keys of an object. Used to limit types when configuring default values in country config. * @example diff --git a/packages/commons/src/events/test.utils.ts b/packages/commons/src/events/test.utils.ts index e92213cfcb8..b0c029b4926 100644 --- a/packages/commons/src/events/test.utils.ts +++ b/packages/commons/src/events/test.utils.ts @@ -187,6 +187,7 @@ function mapFieldTypeToMockValue( case FieldType.OFFICE: case FieldType.LINK_BUTTON: case FieldType.LOADER: + case FieldType.IMAGE: return `${field.id}-${field.type}-${i}` case FieldType.VERIFICATION_STATUS: return 'verified' diff --git a/packages/commons/src/events/utils.ts b/packages/commons/src/events/utils.ts index d6aff90e21f..265eaa44a74 100644 --- a/packages/commons/src/events/utils.ts +++ b/packages/commons/src/events/utils.ts @@ -550,7 +550,11 @@ export function getPendingAction(actions: Action[]): ActionDocument { ) ) - if (pendingActions.length !== 1) { + if (pendingActions.length === 0) { + throw new Error(`Expected exactly one pending action, but found none`) + } + + if (pendingActions.length > 1) { throw new Error( `Expected exactly one pending action, but found ${pendingActions.map(({ id }) => id).join(', ')}` ) diff --git a/packages/components/package.json b/packages/components/package.json index 3398e3d0e5d..55bb26ac9db 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,7 +1,7 @@ { "name": "@opencrvs/components", "main": "lib/index", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS UI Component library", "license": "MPL-2.0", "private": true, diff --git a/packages/components/src/Table/Table.tsx b/packages/components/src/Table/Table.tsx index 6601d257d96..a3d84c72ee9 100644 --- a/packages/components/src/Table/Table.tsx +++ b/packages/components/src/Table/Table.tsx @@ -146,7 +146,7 @@ const ValueWrapper = styled.span<{ alignment?: string color?: string // TODO: The children can be passed a `IDynamicValues` value, which is a very flexible / any-like type. - // eslint-disable-next-line @typescript-eslint/no-explicit-any + children?: any }>` width: ${({ width, totalWidth }) => @@ -209,8 +209,8 @@ const TableScroller = styled.div<{ isFullPage ? `calc(100vh - ${offsetTop}px - 180px)` : height - ? `${height}px` - : 'auto'}; + ? `${height}px` + : 'auto'}; ${({ fixedWidth, totalWidth }) => fixedWidth diff --git a/packages/config/package.json b/packages/config/package.json index 9633a427f41..fa74012a8fb 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/config", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS public configuration microservice", "license": "MPL-2.0", "scripts": { diff --git a/packages/data-seeder/package.json b/packages/data-seeder/package.json index 0962960a3b6..65312b75c3a 100644 --- a/packages/data-seeder/package.json +++ b/packages/data-seeder/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/data-seeder", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS data-seeder microservice", "homepage": "https://github.com/opencrvs/opencrvs-core#readme", "license": "MPL-2.0", diff --git a/packages/documents/package.json b/packages/documents/package.json index 3700214330e..170c3e06537 100644 --- a/packages/documents/package.json +++ b/packages/documents/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/documents", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS Documents service", "license": "MPL-2.0", "private": true, diff --git a/packages/events/package.json b/packages/events/package.json index 100ff899dcb..69630bb0cb5 100644 --- a/packages/events/package.json +++ b/packages/events/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/events", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS Events service", "license": "MPL-2.0", "private": true, @@ -26,6 +26,7 @@ "@types/pg-cursor": "^2.7.2", "app-module-path": "^2.2.0", "envalid": "^8.0.0", + "jose": "^6.1.2", "json-stream-stringify": "^3.1.6", "jsonwebtoken": "^9.0.0", "kysely": "^0.28.2", diff --git a/packages/events/src/environment.ts b/packages/events/src/environment.ts index c011086eecb..71c5f590f5a 100644 --- a/packages/events/src/environment.ts +++ b/packages/events/src/environment.ts @@ -27,5 +27,9 @@ export const env = cleanEnv(process.env, { DOCUMENTS_URL: url({ devDefault: 'http://localhost:9050' }), USER_MANAGEMENT_URL: url({ devDefault: 'http://localhost:3030' }), AUTH_URL: url({ devDefault: 'http://localhost:4040' }), - CONFIG_URL: url({ devDefault: 'http://localhost:2021' }) + CONFIG_URL: url({ devDefault: 'http://localhost:2021' }), + GATEWAY_URL: url({ + devDefault: 'http://localhost:7070', + default: 'https://gateway.vc.e2e-k8s.opencrvs.dev' + }) }) diff --git a/packages/events/src/router/openid4vc/index.ts b/packages/events/src/router/openid4vc/index.ts new file mode 100644 index 00000000000..66d843f6456 --- /dev/null +++ b/packages/events/src/router/openid4vc/index.ts @@ -0,0 +1,99 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ + +import z from 'zod' +import { TRPCError } from '@trpc/server' +import { getTokenPayload, UUID } from '@opencrvs/commons' +import { router, systemProcedure } from '@events/router/trpc' +import { createCredentialOfferUri } from '@events/service/openid4vc/credential-offer' +import { getEventById } from '@events/service/events/events' +import { signBirthCertificateVc } from '@events/service/openid4vc/sign' + +const CredentialRequest = z.object({ + credential_configuration_id: z.string().optional() +}) + +export const openid4vcRouter = router({ + issuance: router({ + offer: systemProcedure + .input(UUID) // eventId, eventually credentialSubject payload + .mutation(({ input }) => { + const offerUri = createCredentialOfferUri(input as UUID) + + return { + credentialOfferUri: offerUri, + deepLink: `openid-credential-offer://?credential_offer_uri=${encodeURIComponent( + offerUri + )}` + } + }), + offers: systemProcedure + .input(UUID) // eventId as "pre-authorized code" in PoC + .query(async ({ input }) => { + const eventId = input as UUID + + // verify that the event exists + await getEventById(eventId) + + // Minimal OID4VCI-style credential_offer object + return { + // where your /.well-known/openid-credential-issuer lives + credential_issuer: + 'http://localhost:7070/.well-known/openid-credential-issuer', + + // which credential configuration(s) this offer is for + credential_configuration_ids: ['BirthCertificateCredential'], + + grants: { + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': eventId, // PoC: using eventId directly + user_pin_required: false + } + } + } + }), + credential: systemProcedure + .input(CredentialRequest) + .mutation(async ({ input, ctx }) => { + // parse eventId token from ctx.token JWT + const { eventId } = getTokenPayload(ctx.token) + + if (!eventId) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'missing eventId in token' + }) + } + + const configId = + input.credential_configuration_id ?? 'BirthCertificateCredential' + + if (configId !== 'BirthCertificateCredential') { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'unsupported_credential_configuration' + }) + } + + const event = await getEventById(eventId) + + const jwtVc = await signBirthCertificateVc({ + subjectId: eventId, // or some subject mapping + event + }) + + return { + format: 'jwt_vc_json', + credential: jwtVc + } + }) + }) +}) diff --git a/packages/events/src/router/router.ts b/packages/events/src/router/router.ts index 52e89c7f68c..ce761ab817f 100644 --- a/packages/events/src/router/router.ts +++ b/packages/events/src/router/router.ts @@ -14,12 +14,14 @@ import { eventRouter } from './event' import { userRouter } from './user' import { locationRouter } from './locations' import { workqueueRouter } from './workqueue' +import { openid4vcRouter } from './openid4vc' export const appRouter = router({ event: eventRouter, user: userRouter, locations: locationRouter, - workqueue: workqueueRouter + workqueue: workqueueRouter, + openid4vc: openid4vcRouter }) /** @knipignore */ diff --git a/packages/events/src/service/indexing/indexing.ts b/packages/events/src/service/indexing/indexing.ts index 2e8ccc9e6a1..5421f147e01 100644 --- a/packages/events/src/service/indexing/indexing.ts +++ b/packages/events/src/service/indexing/indexing.ts @@ -181,6 +181,7 @@ function mapFieldTypeToElasticsearch( option: { type: 'keyword' } } } + case FieldType.IMAGE: case FieldType.SEARCH: case FieldType.ID_READER: case FieldType.QR_READER: diff --git a/packages/events/src/service/openid4vc/BirthCertificate.ts b/packages/events/src/service/openid4vc/BirthCertificate.ts new file mode 100644 index 00000000000..194ad8337ad --- /dev/null +++ b/packages/events/src/service/openid4vc/BirthCertificate.ts @@ -0,0 +1,52 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ + +import z from 'zod' + +export const CredentialSubject = z.object({ + id: z.string(), // child DID/URN + + givenName: z.string(), + middleName: z.string().optional(), + familyName: z.string(), + + gender: z.string(), // use controlled vocab / ISO/ICD + + birthDate: z.string(), // date (YYYY-MM-DD) + birthTime: z.string().optional(), // time (HH:MM) + + birthPlace: z.object({ + name: z.string(), // human-readable + code: z.record(z.string(), z.string()) // e.g. { country, prov, dist } + }), + + plurality: z.number().int().optional(), + birthOrder: z.number().int().optional(), + + // B. Parent / guardian + parent: z + .array( + z.object({ + name: z.string(), + birthDate: z.string().optional(), // date + nationality: z.string().optional(), // ISO-3166-1 alpha-3 + identifier: z.string().optional() // NIN/UIN/NUC + }) + ) + .optional(), + + parentalStatus: z.string().optional(), // marital status at birth + + // G. Local extensions + nationality: z.string().optional(), // child nationality + nationalIdentifier: z.string().optional(), // child UIN/NUC + nameVariant: z.array(z.string()).optional() +}) diff --git a/packages/events/src/service/openid4vc/credential-offer.ts b/packages/events/src/service/openid4vc/credential-offer.ts new file mode 100644 index 00000000000..ebfe639efda --- /dev/null +++ b/packages/events/src/service/openid4vc/credential-offer.ts @@ -0,0 +1,18 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ + +import { UUID } from '@opencrvs/commons' +import { env } from '@events/environment' + +export const createCredentialOfferUri = (eventId: UUID) => { + const input = encodeURIComponent(JSON.stringify(eventId)) + return `${env.GATEWAY_URL}/api/openid4vc.issuance.offers?input=${input}` +} diff --git a/packages/events/src/service/openid4vc/sign.ts b/packages/events/src/service/openid4vc/sign.ts new file mode 100644 index 00000000000..7d8f059d079 --- /dev/null +++ b/packages/events/src/service/openid4vc/sign.ts @@ -0,0 +1,72 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ + +import { EventDocument } from '@opencrvs/commons' + +// For PoC: ES256 keypair in env or config +const issuer = 'http://localhost:7070/openid4vc' // or did:web, etc. + +const privateJwk = { + // minimal PoC; load from env / secrets in real code + kty: 'EC', + crv: 'P-256' + // d, x, y... +} + +// actually `CryptoKey` but for PoC can't import this from Jose +let privateKeyPromise: Promise | null = null +async function getPrivateKey() { + const { importJWK } = await import('jose') + + if (!privateKeyPromise) { + privateKeyPromise = importJWK(privateJwk, 'ES256') + } + return privateKeyPromise +} + +export async function signBirthCertificateVc(params: { + subjectId: string + event: EventDocument +}) { + const { SignJWT } = await import('jose') + const { subjectId, event } = params + const now = Math.floor(Date.now() / 1000) + const exp = now + 10 * 365 * 24 * 60 * 60 // 10 years PoC + + const vcPayload = { + iss: issuer, + sub: subjectId, + nbf: now, + iat: now, + exp, + jti: event.id, // or some VC id + vc: { + '@context': [ + 'https://www.w3.org/2018/credentials/v1' + // your custom context if needed + ], + type: ['VerifiableCredential', 'BirthCertificateCredential'], + credentialSubject: { + id: subjectId, + givenName: 'Pyry', + familyName: 'Rouvila', + dateOfBirth: '1998-01-01', + placeOfBirth: 'Helsinki, Finland' + } + } + } + + const key = await getPrivateKey() + + return new SignJWT(vcPayload) + .setProtectedHeader({ alg: 'ES256', typ: 'JWT' }) + .sign(key) +} diff --git a/packages/gateway/package.json b/packages/gateway/package.json index 536af84d3e9..1734ab71e63 100644 --- a/packages/gateway/package.json +++ b/packages/gateway/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/gateway", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS API Gateway with GraphQL", "license": "MPL-2.0", "scripts": { diff --git a/packages/gateway/src/config/proxies.ts b/packages/gateway/src/config/proxies.ts index 9d161f03876..180ce7abe78 100644 --- a/packages/gateway/src/config/proxies.ts +++ b/packages/gateway/src/config/proxies.ts @@ -8,7 +8,7 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -/* eslint-disable import/no-named-as-default-member */ + import { APPLICATION_CONFIG_URL, AUTH_URL } from '@gateway/constants' import fetch from '@gateway/fetch' import { rateLimitedRoute } from '@gateway/rate-limit' @@ -49,6 +49,7 @@ export const catchAllProxy = { } } }, + /** @deprecated old naming strategy from Hearth. * These are included for backwards compability but `locationS` should be preferred */ location: { @@ -204,6 +205,30 @@ export const authProxy = { parse: false } } + }, + wellKnown: { + method: 'GET', + path: '/.well-known', + handler: (_, h) => + h.proxy({ + uri: `${AUTH_URL}/.well-known`, + passThrough: true + }), + options: { + auth: false + } + }, + wellKnownDirectory: { + method: 'GET', + path: '/.well-known/{suffix}', + handler: (_, h) => + h.proxy({ + uri: `${AUTH_URL}/.well-known/{suffix}`, + passThrough: true + }), + options: { + auth: false + } } } satisfies Record diff --git a/packages/gateway/src/config/routes.ts b/packages/gateway/src/config/routes.ts index 58960031b91..c86b65815d7 100644 --- a/packages/gateway/src/config/routes.ts +++ b/packages/gateway/src/config/routes.ts @@ -135,6 +135,8 @@ export const getRoutes = () => { catchAllProxy.locationId, catchAllProxy.auth, + authProxy.wellKnown, + authProxy.wellKnownDirectory, authProxy.token, rateLimitedAuthProxy.authenticate, rateLimitedAuthProxy.authenticateSuperUser, diff --git a/packages/login/package.json b/packages/login/package.json index 9857949b83c..41e326978bd 100644 --- a/packages/login/package.json +++ b/packages/login/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/login", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS login client application", "license": "MPL-2.0", "private": true, diff --git a/packages/metrics/package.json b/packages/metrics/package.json index 00cd3ff2d01..08c393b3f81 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/metrics", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS metrics service", "license": "MPL-2.0", "private": true, diff --git a/packages/migration/package.json b/packages/migration/package.json index d61b67ef937..ce66e409631 100644 --- a/packages/migration/package.json +++ b/packages/migration/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/migration", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS migration microservice", "homepage": "https://github.com/opencrvs/opencrvs-core#readme", "type": "module", diff --git a/packages/notification/package.json b/packages/notification/package.json index 5000f43ca4b..9285b1b8291 100644 --- a/packages/notification/package.json +++ b/packages/notification/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/notification", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS notification service", "license": "MPL-2.0", "private": true, diff --git a/packages/search/package.json b/packages/search/package.json index 783d8510b46..c32f6c0e7d1 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/search", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS search service", "license": "MPL-2.0", "private": true, diff --git a/packages/toolkit/build.sh b/packages/toolkit/build.sh index ed7df9e93aa..53022420966 100755 --- a/packages/toolkit/build.sh +++ b/packages/toolkit/build.sh @@ -1,3 +1,4 @@ +#!/bin/bash # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -7,8 +8,6 @@ # # Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. -#!/bin/bash - set -e rm -rf dist diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 173cb8e8e14..2e1b864e1fd 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/toolkit", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS toolkit for building country configurations", "license": "MPL-2.0", "exports": { diff --git a/packages/user-mgnt/package.json b/packages/user-mgnt/package.json index 35da75e2b8c..70012f37210 100644 --- a/packages/user-mgnt/package.json +++ b/packages/user-mgnt/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/user-mgnt", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS user management service", "license": "MPL-2.0", "private": true, diff --git a/packages/webhooks/package.json b/packages/webhooks/package.json index 9f306ee3e3c..3adf327e1ef 100644 --- a/packages/webhooks/package.json +++ b/packages/webhooks/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/webhooks", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS webhooks service", "license": "MPL-2.0", "private": true, diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 5a728b3c678..79ed113f83a 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/workflow", - "version": "1.9.0", + "version": "1.9.2", "description": "OpenCRVS workflow service", "license": "MPL-2.0", "private": true, diff --git a/yarn.lock b/yarn.lock index fda9d4fdbc8..5a33510ddb5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10440,7 +10440,7 @@ resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== -"@types/react@*", "@types/react@16 || 17 || 18", "@types/react@18.3.1", "@types/react@>=16", "@types/react@^16": +"@types/react@*", "@types/react@16 || 17 || 18", "@types/react@18.3.1", "@types/react@>=16": version "18.3.1" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.1.tgz#fed43985caa834a2084d002e4771e15dfcbdbe8e" integrity sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw== @@ -10448,6 +10448,15 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/react@^16": + version "16.14.68" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.68.tgz#12127fca0f7623df663ecc570fa2a3b0cc42dbcf" + integrity sha512-GEe60JEJg7wIvnUzXBX/A++ieyum98WXF/q2oFr1RVar8OK8JxU/uEYBXgv7jF87SoaDdxtAq3KUaJFlu02ziw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "^0.16" + csstype "^3.2.2" + "@types/readdir-glob@*": version "1.1.3" resolved "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.3.tgz" @@ -10509,6 +10518,11 @@ dependencies: htmlparser2 "^8.0.0" +"@types/scheduler@^0.16": + version "0.16.8" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" + integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== + "@types/semver@^7.3.4": version "7.5.8" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" @@ -14416,6 +14430,11 @@ csstype@^3.0.2, csstype@^3.1.3: resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +csstype@^3.2.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + csv-parser@^2.3.0: version "2.3.5" resolved "https://registry.yarnpkg.com/csv-parser/-/csv-parser-2.3.5.tgz#6b3bf0907684914ff2c5abfbadab111a69eae5db" @@ -17235,7 +17254,14 @@ fast-url-parser@1.1.3, fast-url-parser@^1.1.3: dependencies: punycode "^1.3.2" -fast-xml-parser@4.2.5, fast-xml-parser@4.4.1, fast-xml-parser@^4.1.3, fast-xml-parser@^4.2.2: +fast-xml-parser@4.2.5: + version "4.2.5" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz#a6747a09296a6cb34f2ae634019bf1738f3b421f" + integrity sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g== + dependencies: + strnum "^1.0.5" + +fast-xml-parser@^4.1.3, fast-xml-parser@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz#86dbf3f18edf8739326447bcaac31b4ae7f6514f" integrity sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw== @@ -20597,6 +20623,11 @@ jose@^5.0.0: resolved "https://registry.yarnpkg.com/jose/-/jose-5.4.1.tgz#b471ee3963920ba5452fd1b1398c8ba72a7b2fcf" integrity sha512-U6QajmpV/nhL9SyfAewo000fkiRQ+Yd2H0lBxJJ9apjpOgkOcBQJWOrMo917lxLptdS/n/o/xPzMkXhF46K8hQ== +jose@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/jose/-/jose-6.1.2.tgz#05f75a3bcdf352d07ebdffcfb11d4e835f6f95c6" + integrity sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ== + joycon@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"