├── .codeclimate.yml ├── .gitattributes ├── .github ├── actions │ ├── build_acapy │ │ └── action.yaml │ ├── build_ui │ │ └── action.yaml │ └── chart_releaser │ │ ├── action.yaml │ │ └── cr.sh ├── cr.yaml ├── ct.yaml ├── dependabot.yml └── workflows │ ├── chart_release.yaml │ ├── delete_images.yaml │ ├── on_pr_closed.yaml │ ├── on_pr_opened.yaml │ ├── on_push_main.yaml │ ├── release_assets.yaml │ ├── stale.yaml │ ├── test_innkeeper_plugin.yaml │ └── test_tenant_ui.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── COMPLIANCE.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── charts ├── README.md └── traction │ ├── Chart.lock │ ├── Chart.yaml │ ├── README.md │ ├── charts │ ├── acapy-0.2.3.tgz │ └── common-2.31.3.tgz │ ├── templates │ ├── _helpers.tpl │ ├── ingress.yaml │ ├── plugin_innkeeper_secret.yaml │ ├── proxy │ │ ├── deployment.yaml │ │ ├── hpa.yaml │ │ ├── networkpolicies.yaml │ │ ├── route.yaml │ │ ├── service.yaml │ │ └── serviceaccount.yaml │ └── ui │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── form-configmap.yaml │ │ ├── hpa.yaml │ │ ├── networkpolicy.yaml │ │ ├── route.yaml │ │ ├── service.yaml │ │ └── serviceaccount.yaml │ └── values.yaml ├── deploy └── traction │ └── values-pr.yaml ├── docs ├── README.md ├── USE-CASE-API-KEY.md ├── USE-CASE-ONBOARD.md ├── assets │ ├── onboard01-reservation.mp4 │ ├── onboard02-denied.mp4 │ ├── onboard03-issuer.mp4 │ ├── readme-logo.png │ ├── tenant-ui-flow-chart-svg.svg │ ├── traction-flow-chart-1600x900-12162022-01.jpg │ └── traction-flow-chart-1600x900-12162022.pdf ├── chart-upgrade-guide.md ├── tenant-ui │ ├── EMDT_001.xd │ ├── EMDT_002.xd │ ├── EMDT_003.xd │ ├── EMDT_004_Presentation.xd │ ├── EMDT_Chat_001.xd │ ├── Email_templates_001.xd │ ├── InnKeeper_001.xd │ ├── Messages_001.xd │ ├── QR_Code_001.xd │ ├── README.md │ ├── RESERVATION_FLOW.md │ ├── Tenant_SelfServe_001.xd │ ├── persona_001.xd │ └── toast_messages.xd └── traction-anoncreds-workshop.md ├── lintconf.yaml ├── plugins ├── .devcontainer │ ├── Dockerfile │ ├── configs │ │ ├── default.yml │ │ └── devcontainer.yml │ ├── devcontainer.json │ ├── docker-compose.devcontainer.yml │ ├── docker-compose.yml │ └── post-install.sh ├── .gitignore ├── .vscode │ ├── launch.json │ └── tasks.json ├── README.md ├── docker │ ├── Dockerfile │ ├── Dockerfile.tenant-proxy │ ├── README.md │ ├── default.yml │ └── tenant-proxy.conf.template └── traction_innkeeper │ ├── README.md │ ├── docker │ ├── Dockerfile │ └── default.yml │ ├── poetry.lock │ ├── pyproject.toml │ ├── setup.cfg │ ├── setup.py │ └── traction_innkeeper │ ├── __init__.py │ ├── definition.py │ └── v1_0 │ ├── __init__.py │ ├── connections │ ├── __init__.py │ └── routes.py │ ├── creddef_storage │ ├── __init__.py │ ├── creddef_storage_service.py │ ├── models.py │ └── routes.py │ ├── endorser │ ├── __init__.py │ ├── endorser_connection_service.py │ └── routes.py │ ├── innkeeper │ ├── __init__.py │ ├── config.py │ ├── models.py │ ├── routes.py │ ├── tenant_manager.py │ └── utils.py │ ├── oca │ ├── __init__.py │ ├── models.py │ ├── oca_service.py │ └── routes.py │ ├── routes.py │ ├── schema_storage │ ├── __init__.py │ ├── models.py │ ├── routes.py │ └── schema_storage_service.py │ ├── tenant │ ├── __init__.py │ ├── holder_revocation_service.py │ └── routes.py │ └── tests │ ├── test_connections_routes.py │ ├── test_creddef_storage_routes.py │ ├── test_creddef_storage_service.py │ ├── test_endorser_connection_service.py │ ├── test_endorser_routes.py │ ├── test_holder_revocation_service.py │ ├── test_innkeeper_routes.py │ ├── test_innkeeper_util.py │ ├── test_oca_routes.py │ ├── test_oca_service.py │ ├── test_schema_storage_routes.py │ ├── test_schema_storage_service.py │ ├── test_tenant_manager.py │ └── test_tenant_routes.py ├── scripts ├── .env-example ├── README.md ├── configs │ └── promtail.yml ├── docker-compose.logs.yml ├── docker-compose.yml ├── endorser-acapy-args.yml ├── ledgers.yml ├── manage └── plugin-config.yml └── services ├── aca-py ├── Dockerfile.acapy └── ngrok-wait.sh └── tenant-ui ├── .devcontainer ├── devcontainer.json └── post-install.sh ├── .dockerignore ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── Dockerfile ├── README.md ├── config ├── custom-environment-variables.json ├── default.json └── forms │ └── reservation.json ├── eslint.config.js ├── frontend ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── forms │ │ └── reservation.json │ └── img │ │ ├── badges │ │ ├── issuer.png │ │ ├── issuer_shield.png │ │ └── wallet.png │ │ ├── bc │ │ ├── bc_logo.png │ │ ├── bc_logo_square.svg │ │ └── bc_logo_white_text.svg │ │ ├── default-login-image.jpg │ │ ├── innkeeper │ │ ├── innkeeper-badge.png │ │ └── innkeeper-login-image.jpg │ │ └── logo │ │ ├── aries-logo-color.png │ │ ├── traction-logo-5.svg │ │ ├── traction-logo-7.svg │ │ ├── traction-logo-bc-text.svg │ │ └── traction-logo-text.svg ├── schema.ts ├── scripts │ ├── i18n.js │ ├── i18n │ │ ├── commonKeys.js │ │ ├── fillKeys.js │ │ ├── sortKeys.js │ │ └── utils.js │ ├── seed-contacts.sh │ └── seed-schemas.sh ├── src │ ├── App.vue │ ├── assets │ │ ├── _theme.scss │ │ ├── layout.scss │ │ ├── primevueOverrides │ │ │ ├── buttons.scss │ │ │ ├── datatable.scss │ │ │ ├── formFields.scss │ │ │ ├── inputSwitch.scss │ │ │ ├── panels.scss │ │ │ └── primevueOverrides.scss │ │ ├── style.scss │ │ ├── tenantuiComponents.scss │ │ ├── toast.scss │ │ ├── variables.scss │ │ └── vue.svg │ ├── components │ │ ├── Login.vue │ │ ├── LoginForm.vue │ │ ├── about │ │ │ ├── Acapy.vue │ │ │ ├── Business.vue │ │ │ ├── PluginList.vue │ │ │ └── Traction.vue │ │ ├── authentications │ │ │ ├── ApiKeys.vue │ │ │ ├── DeleteApiKey.vue │ │ │ └── createApiKey │ │ │ │ ├── CreateApiKey.vue │ │ │ │ └── CreateApiKeyForm.vue │ │ ├── common │ │ │ ├── ConfigItem.vue │ │ │ ├── InfoModal.vue │ │ │ ├── LoadingLabel.vue │ │ │ ├── LocaleSwitcher.vue │ │ │ ├── MenuItemLink.vue │ │ │ ├── PanelMenuItemLink.vue │ │ │ ├── QRCode.vue │ │ │ ├── RowExpandData.vue │ │ │ ├── SessionTimeoutModal.vue │ │ │ ├── SessionTimer.vue │ │ │ ├── SkeletonCard.vue │ │ │ ├── StatusChip.vue │ │ │ ├── ToggleJson.vue │ │ │ └── ValidatedField.vue │ │ ├── connections │ │ │ ├── Connections.vue │ │ │ ├── Invitations.vue │ │ │ ├── acceptInvitation │ │ │ │ ├── AcceptInvitation.vue │ │ │ │ ├── AcceptInviteForm.vue │ │ │ │ └── AcceptInviteSubmission.vue │ │ │ ├── createConnection │ │ │ │ ├── CreateConnection.vue │ │ │ │ ├── CreateConnectionForm.vue │ │ │ │ └── RegenerateInvitation.vue │ │ │ ├── didExchange │ │ │ │ ├── DidExchange.vue │ │ │ │ └── DidExchangeForm.vue │ │ │ ├── editConnection │ │ │ │ ├── DeleteConnection.vue │ │ │ │ ├── EditConnection.vue │ │ │ │ └── EditConnectionForm.vue │ │ │ └── messageConnection │ │ │ │ ├── MessageConnection.vue │ │ │ │ └── MessageConnectionList.vue │ │ ├── holder │ │ │ ├── CredentialAttributes.vue │ │ │ ├── Credentials.vue │ │ │ ├── CredentialsCards.vue │ │ │ ├── CredentialsTable.vue │ │ │ └── credentialOcaCard │ │ │ │ ├── Attribute.vue │ │ │ │ ├── CardLogo.vue │ │ │ │ ├── CardPrimaryBody.vue │ │ │ │ ├── CardSecondaryBody.vue │ │ │ │ ├── CredentialCard10.vue │ │ │ │ ├── CredentialName.vue │ │ │ │ ├── IssuerName.vue │ │ │ │ ├── OcaCard.vue │ │ │ │ └── OcaStyleConstants.ts │ │ ├── innkeeper │ │ │ ├── InnkeeperBadge.vue │ │ │ ├── InnkeeperLogin.vue │ │ │ ├── InnkeeperLoginForm.vue │ │ │ ├── InnkeeperLoginOidc.vue │ │ │ ├── authentications │ │ │ │ ├── ApiKeys.vue │ │ │ │ ├── DeleteApiKey.vue │ │ │ │ └── createApiKey │ │ │ │ │ ├── CreateApiKey.vue │ │ │ │ │ └── CreateApiKeyForm.vue │ │ │ ├── config │ │ │ │ └── ServerConfig.vue │ │ │ ├── reservation │ │ │ │ ├── ApproveReservation.vue │ │ │ │ ├── DenyReservation.vue │ │ │ │ ├── RefreshPassword.vue │ │ │ │ ├── Reservations.vue │ │ │ │ └── ReservationsHistory.vue │ │ │ └── tenants │ │ │ │ ├── Tenants.vue │ │ │ │ ├── deleteTenant │ │ │ │ ├── ConfirmTenantDeletion.vue │ │ │ │ ├── DeleteTenant.vue │ │ │ │ └── RestoreTenant.vue │ │ │ │ └── editConfig │ │ │ │ ├── editConfig.vue │ │ │ │ └── editConfigForm.vue │ │ ├── issuance │ │ │ ├── credentialDefinitions │ │ │ │ ├── CopyCredentialDefinitionForm.vue │ │ │ │ ├── CreateCredentialDefinition.vue │ │ │ │ ├── CreateCredentialDefinitionForm.vue │ │ │ │ ├── CredentialDefinitions.vue │ │ │ │ ├── NestedCredentialDefinition.vue │ │ │ │ └── checkCredDefPostedInterval.ts │ │ │ ├── credentials │ │ │ │ ├── DeleteCredentialExchangeButton.vue │ │ │ │ ├── EnterCredentialValues.vue │ │ │ │ ├── IssuedCredentials.vue │ │ │ │ ├── OfferCredential.vue │ │ │ │ ├── OfferCredentialForm.vue │ │ │ │ ├── RevokeCredentialButton.vue │ │ │ │ └── RevokeCredentialForm.vue │ │ │ └── schemas │ │ │ │ ├── AddSchemaFromLedger.vue │ │ │ │ ├── AddSchemaFromLedgerForm.vue │ │ │ │ ├── Attributes.vue │ │ │ │ ├── CopySchema.vue │ │ │ │ ├── CreateSchema.vue │ │ │ │ ├── CreateSchemaForm.vue │ │ │ │ ├── Schemas.vue │ │ │ │ └── checkSchemaPostedInterval.ts │ │ ├── layout │ │ │ ├── AppLayout.vue │ │ │ ├── Footer.vue │ │ │ ├── Header.vue │ │ │ ├── Sidebar.vue │ │ │ ├── innkeeper │ │ │ │ ├── Header.vue │ │ │ │ ├── InnkeeperLayout.vue │ │ │ │ └── Sidebar.vue │ │ │ └── mainCard │ │ │ │ ├── ExpandButton.vue │ │ │ │ ├── MainCard.vue │ │ │ │ └── MainCardContent.vue │ │ ├── messages │ │ │ ├── Messages.vue │ │ │ └── createMessage │ │ │ │ ├── CreateMessage.vue │ │ │ │ └── CreateMessageForm.vue │ │ ├── notifications │ │ │ └── Alert.vue │ │ ├── oca │ │ │ ├── Oca.vue │ │ │ └── createOca │ │ │ │ ├── CreateOca.vue │ │ │ │ └── CreateOcaForm.vue │ │ ├── oidc │ │ │ └── LoginOIDC.vue │ │ ├── profile │ │ │ ├── Developer.vue │ │ │ ├── JWT.vue │ │ │ ├── ProfileButton.vue │ │ │ ├── ProfileFooter.vue │ │ │ ├── ProfileForm.vue │ │ │ ├── SettingsForm.vue │ │ │ └── issuance │ │ │ │ ├── Endorser.vue │ │ │ │ ├── EndorserConnect.vue │ │ │ │ ├── Issuance.vue │ │ │ │ ├── PublicDid.vue │ │ │ │ ├── PublicDidRegister.vue │ │ │ │ ├── ReviewTaa.vue │ │ │ │ └── TaaAcceptance.vue │ │ ├── reservation │ │ │ ├── ReservationConfirmation.vue │ │ │ ├── Reserve.vue │ │ │ ├── Status.vue │ │ │ ├── status │ │ │ │ ├── Approved.vue │ │ │ │ ├── CheckedIn.vue │ │ │ │ ├── Denied.vue │ │ │ │ ├── NotFound.vue │ │ │ │ ├── Pending.vue │ │ │ │ └── ShowWallet.vue │ │ │ └── user │ │ │ │ └── OidcUserDisplay.vue │ │ └── verifier │ │ │ ├── DeleteExchangeRecord.vue │ │ │ ├── PresentationRowExpandData.vue │ │ │ ├── Presentations.vue │ │ │ ├── VerifiedPresentationData.vue │ │ │ └── createPresentationRequest │ │ │ ├── CreateRequest.vue │ │ │ └── CreateRequestForm.vue │ ├── composables │ │ └── useGetItem.ts │ ├── helpers │ │ ├── constants.ts │ │ ├── errorHandler.ts │ │ ├── index.ts │ │ ├── jsonParsing.ts │ │ └── tableFormatters.ts │ ├── main.ts │ ├── matomoSetup.ts │ ├── overlayLibrary │ │ ├── contexts │ │ │ └── Branding.ts │ │ ├── interfaces │ │ │ └── overlay │ │ │ │ ├── BrandingOverlayData.interface.ts │ │ │ │ ├── OverlayBundleData.interface.ts │ │ │ │ ├── OverlayData.interface.ts │ │ │ │ └── SemanticOverlay.interface.ts │ │ ├── services │ │ │ ├── OverlayBrandingDataFactory.ts │ │ │ └── OverlayBundleFactory.ts │ │ ├── types │ │ │ └── overlay │ │ │ │ ├── BrandingOverlay.ts │ │ │ │ ├── Overlay.ts │ │ │ │ ├── OverlayBundle.ts │ │ │ │ ├── OverlayType.ts │ │ │ │ └── SemanticOverlay.ts │ │ └── utils │ │ │ ├── color.ts │ │ │ └── localeDefaults.ts │ ├── plugins │ │ ├── i18n │ │ │ ├── i18n.ts │ │ │ └── locales │ │ │ │ ├── en.json │ │ │ │ ├── fr.json │ │ │ │ └── ja.json │ │ └── toasts │ │ │ └── vueToastification.ts │ ├── router │ │ ├── index.ts │ │ ├── innkeeperRoutes.ts │ │ └── tenantRoutes.ts │ ├── store │ │ ├── acapyApi.ts │ │ ├── commonStore.ts │ │ ├── configStore.ts │ │ ├── connectionStore.ts │ │ ├── governanceStore.ts │ │ ├── holderStore.ts │ │ ├── index.ts │ │ ├── innkeeper │ │ │ ├── innkeeperOidcStore.ts │ │ │ ├── innkeeperTenantsStore.ts │ │ │ └── innkeeperTokenStore.ts │ │ ├── issuerStore.ts │ │ ├── keyManagementStore.ts │ │ ├── logStore.ts │ │ ├── messageStore.ts │ │ ├── oidc │ │ │ └── oidcStore.ts │ │ ├── reservationStore.ts │ │ ├── tenantStore.ts │ │ ├── tokenStore.ts │ │ ├── utils │ │ │ ├── fetchItem.ts │ │ │ ├── fetchList.ts │ │ │ └── index.ts │ │ └── verifierStore.ts │ ├── types │ │ ├── README.md │ │ ├── acapyApi │ │ │ └── acapyInterface.ts │ │ └── index.ts │ ├── views │ │ ├── About.vue │ │ ├── ApiKeys.vue │ │ ├── Dashboard.vue │ │ ├── InnkeeperUi.vue │ │ ├── Log.vue │ │ ├── NotFound.vue │ │ ├── TenantUi.vue │ │ ├── connections │ │ │ ├── MyConnections.vue │ │ │ └── MyInvitations.vue │ │ ├── holder │ │ │ └── MyHeldCredentials.vue │ │ ├── innkeeper │ │ │ ├── InnkeeperApiKeys.vue │ │ │ ├── InnkeeperReservations.vue │ │ │ ├── InnkeeperReservationsHistory.vue │ │ │ ├── InnkeeperServerConfig.vue │ │ │ └── InnkeeperTenants.vue │ │ ├── issuance │ │ │ ├── CredentialDefinitions.vue │ │ │ ├── MyIssuedCredentials.vue │ │ │ └── Schemas.vue │ │ ├── messages │ │ │ └── MyMessages.vue │ │ ├── oca │ │ │ └── Oca.vue │ │ ├── tenant │ │ │ ├── Developer.vue │ │ │ ├── Profile.vue │ │ │ └── Settings.vue │ │ └── verification │ │ │ └── MyPresentations.vue │ └── vite-env.d.ts ├── test │ ├── App.test.ts │ ├── README.md │ ├── __mocks__ │ │ ├── api │ │ │ ├── responses │ │ │ │ ├── acapy.ts │ │ │ │ ├── config.ts │ │ │ │ ├── connection.ts │ │ │ │ ├── governance.ts │ │ │ │ ├── holder.ts │ │ │ │ ├── index.ts │ │ │ │ ├── innkeeper │ │ │ │ │ ├── tenant.ts │ │ │ │ │ └── token.ts │ │ │ │ ├── issuer.ts │ │ │ │ ├── message.ts │ │ │ │ ├── reservation.ts │ │ │ │ ├── tenant.ts │ │ │ │ ├── token.ts │ │ │ │ └── verifier.ts │ │ │ └── routes │ │ │ │ ├── acapy.ts │ │ │ │ ├── config.ts │ │ │ │ ├── connection.ts │ │ │ │ ├── governance.ts │ │ │ │ ├── holder.ts │ │ │ │ ├── index.ts │ │ │ │ ├── innkeeper │ │ │ │ ├── tenant.ts │ │ │ │ └── token.ts │ │ │ │ ├── issuer.ts │ │ │ │ ├── message.ts │ │ │ │ ├── reservation.ts │ │ │ │ ├── tenant.ts │ │ │ │ ├── token.ts │ │ │ │ ├── utils │ │ │ │ └── utils.ts │ │ │ │ └── verifier.ts │ │ ├── store │ │ │ ├── common.ts │ │ │ ├── config.ts │ │ │ ├── connection.ts │ │ │ ├── governance.ts │ │ │ ├── holder.ts │ │ │ ├── index.ts │ │ │ ├── innkeeper │ │ │ │ ├── oidc.ts │ │ │ │ ├── tenants.ts │ │ │ │ └── token.ts │ │ │ ├── issuer.ts │ │ │ ├── message.ts │ │ │ ├── oidc │ │ │ │ └── oidc.ts │ │ │ ├── reservation.ts │ │ │ ├── tenant.ts │ │ │ ├── token.ts │ │ │ └── verifier.ts │ │ └── validation │ │ │ └── forms.ts │ ├── commonTests.ts │ ├── components │ │ ├── Login.test.ts │ │ ├── LoginForm.test.ts │ │ ├── about │ │ │ ├── Acapy.test.ts │ │ │ ├── Business.test.ts │ │ │ ├── Traction.test.ts │ │ │ └── __snapshots__ │ │ │ │ ├── Acapy.test.ts.snap │ │ │ │ ├── Business.test.ts.snap │ │ │ │ └── Traction.test.ts.snap │ │ ├── common │ │ │ ├── ConfigItem.test.ts │ │ │ ├── InfoModal.test.ts │ │ │ ├── LoadingLabel.test.ts │ │ │ ├── LocaleSwitcher.test.ts │ │ │ ├── QRCode.test.ts │ │ │ ├── RowExpandData.test.ts │ │ │ ├── SessionTimeoutModal.test.ts │ │ │ ├── SessionTimer.test.ts │ │ │ ├── SkeletonCard.test.ts │ │ │ ├── StatusChip.test.ts │ │ │ ├── ToggleJson.test.ts │ │ │ └── __snapshots__ │ │ │ │ ├── LocaleSwitcher.test.ts.snap │ │ │ │ └── SkeletonCard.test.ts.snap │ │ ├── connections │ │ │ ├── Connections.test.ts │ │ │ ├── Invitations.test.ts │ │ │ ├── acceptInvitation │ │ │ │ ├── AcceptInvitation.test.ts │ │ │ │ ├── AcceptInviteForm.test.ts │ │ │ │ └── AcceptInviteSubmission.test.ts │ │ │ ├── createContact │ │ │ │ ├── CreateContact.test.ts │ │ │ │ ├── CreateContactForm.test.ts │ │ │ │ └── RegenerateInvitation.test.ts │ │ │ ├── didExchange │ │ │ │ ├── DidExchange.test.ts │ │ │ │ └── DidExchangeForm.test.ts │ │ │ ├── editContact │ │ │ │ ├── DeleteContact.test.ts │ │ │ │ ├── EditContact.test.ts │ │ │ │ └── EditContactForm.test.ts │ │ │ └── messageContact │ │ │ │ ├── MessageContact.test.ts │ │ │ │ └── MessageContactList.test.ts │ │ ├── holder │ │ │ └── CredentialTable.test.ts │ │ ├── innkeeper │ │ │ ├── InnkeeperLogin.test.ts │ │ │ ├── InnkeeperLoginForm.test.ts │ │ │ ├── InnkeeperLoginOidc.test.ts │ │ │ └── tenants │ │ │ │ └── Tenants.test.ts │ │ ├── issuance │ │ │ ├── credentialDefinitions │ │ │ │ └── CredentialDefinitions.test.ts │ │ │ ├── credentials │ │ │ │ └── IssuedCredentials.test.ts │ │ │ └── schemas │ │ │ │ └── Schemas.test.ts │ │ ├── layout │ │ │ ├── Footer.test.ts │ │ │ ├── Header.test.ts │ │ │ ├── Sidebar.test.ts │ │ │ ├── __snapshots__ │ │ │ │ ├── AppLayout.test.ts.snap │ │ │ │ ├── Footer.test.ts.snap │ │ │ │ ├── Header.test.ts.snap │ │ │ │ └── Sidebar.test.ts.snap │ │ │ └── innkeeper │ │ │ │ ├── Header.test.ts │ │ │ │ └── __snapshots__ │ │ │ │ └── Header.test.ts.snap │ │ ├── messages │ │ │ ├── Messages.test.ts │ │ │ └── createMessage │ │ │ │ ├── CreateMessage.test.ts │ │ │ │ └── CreateMessageForm.test.ts │ │ └── verifier │ │ │ └── Presentations.test.ts │ ├── helpers │ │ └── index.test.ts │ ├── router │ │ └── index.test.ts │ ├── setupApi.ts │ ├── setupGlobalMocks.ts │ └── store │ │ ├── acapyApi.test.ts │ │ ├── commonStore.test.ts │ │ ├── configStore.test.ts │ │ ├── contactsStore.test.ts │ │ ├── governanceStore.test.ts │ │ ├── holderStore.test.ts │ │ ├── innkeeper │ │ ├── innkeeperOidcStore.test.ts │ │ ├── innkeeperTenantsStore.test.ts │ │ └── innkeeperTokenStore.test.ts │ │ ├── issuerStore.test.ts │ │ ├── messageStore.test.ts │ │ ├── reservationStore.test.ts │ │ ├── tenantStore.test.ts │ │ ├── tokenStore.test.ts │ │ ├── utils │ │ └── index.test.ts │ │ └── verifierStore.test.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite-env.d.ts └── vite.config.ts ├── package-lock.json ├── package.json ├── src ├── components │ ├── email.ts │ ├── email_templates │ │ ├── README.md │ │ ├── reservation_approved_tenant.ts │ │ ├── reservation_declined_tenant.ts │ │ ├── reservation_received_innkeeper.ts │ │ └── reservation_received_tenant.ts │ └── innkeeper.ts ├── helpers │ ├── constants.ts │ ├── index.ts │ └── startbanner.ts ├── index.ts ├── middleware │ └── oidcMiddleware.ts ├── routes │ └── router.ts └── services │ └── log-stream.ts ├── test ├── components │ └── email_templates │ │ └── email-templates.test.ts ├── helpers │ ├── constants.spec.ts │ ├── index.spec.ts │ └── startbanner.spec.ts └── middleware │ └── oidcMiddleware.spec.ts ├── tsconfig.json ├── tslint.json └── vitest.config.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Autodetect text files and forces unix eols, so Windows does not break them 2 | * text=auto eol=lf 3 | # Force images/fonts to be handled as binaries 4 | *.jpg binary 5 | *.jpeg binary 6 | *.gif binary 7 | *.png binary 8 | *.mp4 binary filter=lfs diff=lfs merge=lfs 9 | -------------------------------------------------------------------------------- /.github/cr.yaml: -------------------------------------------------------------------------------- 1 | owner: bcgov 2 | git-repo: traction 3 | git-base-url: https://api.github.com/ 4 | git-upload-url: https://uploads.github.com/ 5 | -------------------------------------------------------------------------------- /.github/ct.yaml: -------------------------------------------------------------------------------- 1 | # See https://github.com/helm/chart-testing#configuration 2 | remote: origin 3 | target-branch: main 4 | chart-dirs: 5 | - charts 6 | chart-repos: 7 | - bitnami=https://charts.bitnami.com/bitnami 8 | helm-extra-args: --timeout 600s -------------------------------------------------------------------------------- /.github/workflows/delete_images.yaml: -------------------------------------------------------------------------------- 1 | name: Delete old container images 2 | 3 | on: 4 | # schedule: 5 | # - cron: "0 2 * * 2" # This job runs every Tuesday 6 | workflow_dispatch: 7 | 8 | jobs: 9 | clean-ghcr: 10 | name: Delete old unused container images 11 | runs-on: ubuntu-22.04 12 | if: github.repository_owner == 'bcgov' 13 | steps: 14 | - name: Delete container images older than a Month 15 | uses: snok/container-retention-policy@v3.0.1 16 | with: 17 | account: bcgov 18 | image-names: traction-plugins-acapy, traction-tenant-proxy, traction-tenant-ui 19 | cut-off: 1month 20 | tag-selection: untagged 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | name: Close stale issues and PRs 2 | 3 | on: 4 | schedule: 5 | - cron: '30 1 * * *' # This job runs at 1:30 UTC 6 | workflow_dispatch: 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-22.04 11 | if: github.repository_owner == 'bcgov' 12 | steps: 13 | - uses: actions/stale@v10 14 | with: 15 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 16 | stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.' 17 | close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' 18 | close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.' 19 | days-before-issue-stale: 30 20 | days-before-pr-stale: 45 21 | days-before-issue-close: 5 22 | days-before-pr-close: 10 23 | stale-issue-label: 'stale' 24 | exempt-issue-labels: 'pinned,bug,security,dependencies,epic' 25 | stale-pr-label: 'stale' 26 | exempt-pr-labels: 'awaiting-approval,work-in-progress,dependencies' 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | devops/helm/charts/traction/charts/** 3 | **/.env 4 | .DS_Store 5 | 6 | traction-venv 7 | ./services/tenant-ui/config/local.json 8 | 9 | .vscode/settings.json 10 | **/.venv 11 | plugins/poetry.lock 12 | 13 | .idea 14 | **/.idea 15 | .python-version 16 | -------------------------------------------------------------------------------- /COMPLIANCE.yaml: -------------------------------------------------------------------------------- 1 | name: compliance 2 | description: | 3 | This document is used to track a projects PIA and STRA 4 | compliance. 5 | spec: 6 | - name: PIA 7 | status: TBD 8 | last-updated: '2021-11-22T17:53:12.451Z' 9 | - name: STRA 10 | status: TBD 11 | last-updated: '2021-11-22T17:53:12.451Z' 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute 2 | 3 | You are encouraged to contribute to the repository by **forking and submitting a pull request**. 4 | 5 | For significant changes, please open an issue first to discuss the proposed changes to avoid re-work. 6 | 7 | (If you are new to GitHub, you might start with a [basic tutorial](https://help.github.com/articles/set-up-git) and check out a more detailed guide to [pull requests](https://help.github.com/articles/using-pull-requests/).) 8 | 9 | Pull requests will be evaluated by the repository guardians on a schedule and if deemed beneficial will be committed to the `main` branch. Pull requests should have a descriptive name, include an summary of all changes made in the pull request description, and include unit tests that provide good coverage of the feature or fix. A Continuous Integration (CI) pipeline is executed on all PRs before review and contributors are expected to address all CI issues identified. Where appropriate, PRs that impact the 10 | end-user and developer demos in the repo should include updates or extensions to those demos to cover the new capabilities. 11 | 12 | If you would like to propose a significant change, please open an issue first to discuss the work with the community. 13 | 14 | Contributions are made pursuant to the Developer's Certificate of Origin, available at [https://developercertificate.org](https://developercertificate.org), and licensed under the Apache License, version 2.0 (Apache-2.0). 15 | -------------------------------------------------------------------------------- /charts/traction/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: acapy 3 | repository: https://openwallet-foundation.github.io/helm-charts/ 4 | version: 0.2.3 5 | - name: common 6 | repository: https://charts.bitnami.com/bitnami 7 | version: 2.31.3 8 | digest: sha256:971cb1f7db2787651e3b8cf1ea87d7bc4578292d14558ca83eefe00d885f50d7 9 | generated: "2025-09-16T11:29:10.963956-07:00" 10 | -------------------------------------------------------------------------------- /charts/traction/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: traction 3 | description: The Traction service allows organizations to verify, hold, and issue verifiable credentials. The Traction Tenant UI allows tenants to manage their agent. 4 | type: application 5 | version: 0.4.2 6 | appVersion: 1.2.2 7 | home: "https://github.com/bcgov/traction" 8 | sources: ["https://github.com/bcgov/traction"] 9 | icon: "https://github.com/bcgov/traction/raw/main/docs/assets/readme-logo.png" 10 | maintainers: 11 | - email: ivan.polchenko@quartech.com 12 | name: i5okie 13 | url: https://github.com/i5okie 14 | - email: emiliano.sune@quartech.com 15 | name: esune 16 | url: https://github.com/esune 17 | - email: lucasoneil@gmail.com 18 | name: loneil 19 | url: https://github.com/loneil 20 | dependencies: 21 | - name: acapy 22 | version: 0.2.3 23 | repository: https://openwallet-foundation.github.io/helm-charts/ 24 | - name: common 25 | repository: "https://charts.bitnami.com/bitnami" 26 | tags: 27 | - bitnami-common 28 | version: 2.31.3 29 | -------------------------------------------------------------------------------- /charts/traction/charts/acapy-0.2.3.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/charts/traction/charts/acapy-0.2.3.tgz -------------------------------------------------------------------------------- /charts/traction/charts/common-2.31.3.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/charts/traction/charts/common-2.31.3.tgz -------------------------------------------------------------------------------- /charts/traction/templates/plugin_innkeeper_secret.yaml: -------------------------------------------------------------------------------- 1 | {{- if not .Values.ui.pluginInnkeeper.existingSecret }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ include "traction.acapy.plugin.innkeeper.name" . }} 6 | labels: 7 | {{- include "common.labels" . | nindent 4 }} 8 | type: Opaque 9 | data: 10 | INNKEEPER_WALLET_TENANT_ID: {{ include "getOrGenerateUUID" (dict "Name" (include "traction.acapy.plugin.innkeeper.name" .) "Key" "tenantid" "Namespace" .Release.Namespace "Kind" "Secret") }} 11 | INNKEEPER_WALLET_WALLET_KEY: {{ include "getOrGeneratePass" (dict "Length" 32 "Name" (include "traction.acapy.plugin.innkeeper.name" .) "Key" "walletkey" "Namespace" .Release.Namespace "Kind" "Secret") }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /charts/traction/templates/proxy/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.tenant_proxy.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "tenant_proxy.fullname" . }} 6 | labels: 7 | {{- include "tenant_proxy.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "tenant_proxy.fullname" . }} 13 | minReplicas: {{ .Values.tenant_proxy.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.tenant_proxy.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.tenant_proxy.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | target: 21 | type: Utilization 22 | averageUtilization: {{ .Values.tenant_proxy.autoscaling.targetCPUUtilizationPercentage }} 23 | {{- end }} 24 | {{- if .Values.tenant_proxy.autoscaling.targetMemoryUtilizationPercentage }} 25 | - type: Resource 26 | resource: 27 | name: memory 28 | target: 29 | type: Utilization 30 | averageUtilization: {{ .Values.tenant_proxy.autoscaling.targetMemoryUtilizationPercentage }} 31 | {{- end }} 32 | behavior: 33 | scaleDown: 34 | stabilizationWindowSeconds: {{ .Values.tenant_proxy.autoscaling.stabilizationWindowSeconds }} 35 | {{- end }} 36 | -------------------------------------------------------------------------------- /charts/traction/templates/proxy/route.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.tenant_proxy.openshift.route.enabled -}} 2 | {{- $fullName := include "tenant_proxy.fullname" . -}} 3 | apiVersion: route.openshift.io/v1 4 | kind: Route 5 | metadata: 6 | name: {{ include "tenant_proxy.fullname" . }} 7 | annotations: 8 | haproxy.router.openshift.io/timeout: {{ .Values.tenant_proxy.openshift.route.timeout }} 9 | labels: 10 | {{- include "tenant_proxy.labels" . | nindent 4 }} 11 | spec: 12 | host: {{ include "tenant_proxy.host" . | quote }} 13 | path: {{ .Values.tenant_proxy.openshift.route.path }} 14 | to: 15 | kind: Service 16 | name: {{ $fullName }} 17 | weight: 100 18 | port: 19 | targetPort: {{ .Values.tenant_proxy.openshift.route.targetPort }} 20 | wildcardPolicy: {{ .Values.tenant_proxy.openshift.route.wildcardPolicy }} 21 | {{ include "tenant_proxy.openshift.route.tls" . | indent 2}} 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /charts/traction/templates/proxy/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "tenant_proxy.fullname" . }} 5 | labels: 6 | {{- include "tenant_proxy.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.tenant_proxy.service.type }} 9 | ports: 10 | - port: {{ .Values.tenant_proxy.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "tenant_proxy.selectorLabels" . | nindent 4 }} -------------------------------------------------------------------------------- /charts/traction/templates/proxy/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.tenant_proxy.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "tenant_proxy.serviceAccountName" . }} 6 | labels: 7 | {{- include "tenant_proxy.labels" . | nindent 4 }} 8 | {{- with .Values.tenant_proxy.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /charts/traction/templates/ui/form-configmap.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ui.enabled -}} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ include "tenant-ui.fullname" . }}-form 6 | labels: 7 | {{- include "tenant-ui.labels" . | nindent 4 }} 8 | data: 9 | reservation.json: |- 10 | {{- .Values.ui.oidc.reservationForm | nospace | nindent 4 }} 11 | {{- end -}} 12 | -------------------------------------------------------------------------------- /charts/traction/templates/ui/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.ui.enabled .Values.ui.autoscaling.enabled -}} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "tenant-ui.fullname" . }} 6 | labels: 7 | {{- include "tenant-ui.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "tenant-ui.fullname" . }} 13 | minReplicas: {{ .Values.ui.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.ui.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.ui.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | target: 21 | type: Utilization 22 | averageUtilization: {{ .Values.ui.autoscaling.targetCPUUtilizationPercentage }} 23 | {{- end }} 24 | {{- if .Values.ui.autoscaling.targetMemoryUtilizationPercentage }} 25 | - type: Resource 26 | resource: 27 | name: memory 28 | target: 29 | type: Utilization 30 | averageUtilization: {{ .Values.ui.autoscaling.targetMemoryUtilizationPercentage }} 31 | {{- end }} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /charts/traction/templates/ui/networkpolicy.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.ui.enabled .Values.ui.networkPolicy.enabled -}} 2 | apiVersion: networking.k8s.io/v1 3 | kind: NetworkPolicy 4 | metadata: 5 | name: {{ include "tenant-ui.fullname" . }}-ingress 6 | labels: 7 | {{- include "tenant-ui.labels" . | nindent 4 }} 8 | spec: 9 | podSelector: 10 | matchLabels: 11 | {{- include "tenant-ui.selectorLabels" . | nindent 6 }} 12 | ingress: 13 | {{- if and .Values.ingress.enabled .Values.ui.networkPolicy.ingress.enabled (or .Values.ui.networkPolicy.ingress.namespaceSelector .Values.ui.networkPolicy.ingress.podSelector) }} 14 | - from: 15 | {{- if .Values.ui.networkPolicy.ingress.namespaceSelector }} 16 | - namespaceSelector: 17 | matchLabels: 18 | {{- include "common.tplvalues.render" (dict "value" .Values.ui.networkPolicy.ingress.namespaceSelector "context" $) | nindent 14 }} 19 | {{- end }} 20 | {{- if .Values.ui.networkPolicy.ingress.podSelector }} 21 | - podSelector: 22 | matchLabels: 23 | {{- include "common.tplvalues.render" (dict "value" .Values.ui.networkPolicy.ingress.podSelector "context" $) | nindent 14 }} 24 | {{- end }} 25 | {{- end }} 26 | policyTypes: 27 | - Ingress 28 | {{- end -}} 29 | -------------------------------------------------------------------------------- /charts/traction/templates/ui/route.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.ui.enabled .Values.ui.openshift.route.enabled -}} 2 | {{- $fullName := include "tenant-ui.fullname" . -}} 3 | apiVersion: route.openshift.io/v1 4 | kind: Route 5 | metadata: 6 | name: {{ include "tenant-ui.fullname" . }} 7 | annotations: 8 | haproxy.router.openshift.io/timeout: {{ .Values.ui.openshift.route.timeout }} 9 | labels: 10 | {{- include "tenant-ui.labels" . | nindent 4 }} 11 | spec: 12 | host: {{ include "tenant-ui.host" . | quote }} 13 | path: {{ .Values.ui.openshift.route.path }} 14 | to: 15 | kind: Service 16 | name: {{ $fullName }} 17 | weight: 100 18 | port: 19 | targetPort: {{ .Values.ui.openshift.route.targetPort }} 20 | wildcardPolicy: {{ .Values.ui.openshift.route.wildcardPolicy }} 21 | {{ include "tenant-ui.openshift.route.tls" . | indent 2}} 22 | {{- end -}} 23 | -------------------------------------------------------------------------------- /charts/traction/templates/ui/service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ui.enabled -}} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include "tenant-ui.fullname" . }} 6 | labels: 7 | {{- include "tenant-ui.labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .Values.ui.service.type }} 10 | ports: 11 | - port: {{ .Values.ui.service.httpPort }} 12 | targetPort: http 13 | protocol: TCP 14 | name: http 15 | selector: 16 | {{- include "tenant-ui.selectorLabels" . | nindent 4 }} 17 | {{- end -}} 18 | -------------------------------------------------------------------------------- /charts/traction/templates/ui/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.ui.enabled .Values.ui.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "tenant-ui.serviceAccountName" . }} 6 | labels: 7 | {{- include "tenant-ui.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Traction Documentation and Resources 2 | 3 | ## A Traction Sandbox ACA-Py/AnonCreds Workshop 4 | 5 | This workshop contains a sequence of labs demonstrating issuing, receiving, 6 | holding, requesting, presenting, and verifying AnonCreds Verifiable 7 | Credentials--no technical experience required! The labs take about 20 minutes 8 | for complete. New developers expecting to build an Issuer or Verifier with 9 | [Traction] or [ACA-Py] will find this a great place to 10 | start--with developer-oriented "Next Steps" suggested at the end of the 11 | Workshop. 12 | 13 | [Traction]: https://digital.gov.bc.ca/digital-trust/technical-resources/traction/ 14 | [ACA-Py]: https://aca-py.org 15 | 16 | ## Basic Architecture Overview 17 | See [traction flow chart](assets/traction-flow-chart-1600x900-12162022.pdf) 18 | 19 | ## Use Cases 20 | 21 | * [Tenant Onboarding](USE-CASE-ONBOARD.md) 22 | * [API Keys](USE-CASE-API-KEY.md) 23 | 24 | Stay tuned for upcoming project documentation. 25 | -------------------------------------------------------------------------------- /docs/USE-CASE-ONBOARD.md: -------------------------------------------------------------------------------- 1 | # Use Case: Tenant Onboarding 2 | The following videos demonstrate the self-serve onboarding process for a Traction Tenant using the Tenant UI interface. 3 | 4 | 3 "personas" are demonstrated in these demos 5 | 6 | 1. A prospective Tenant. A user that wishes to request a Tenant be created for their use. This user makes use of publicly accessible pages on the Tenant UI without authenticating. 7 | 2. A Tenant. Once the Tenant has been created, the user can use the Tenant UI as a representative of that Tenant. 8 | 3. An Innkeeper user. One of the operational or adminsitrative team members that can control access to the Traction system 9 | 10 | ### Reservation 11 | A user wishing to create a Tenant makes a Reservation outlining their request and the Innkeeper approves it. The user can then create the Tenant and obtain the credentials. 12 | 13 | [Watch it here!](./assets/onboard01-reservation.mp4) 14 | 15 | ### Deny Reservation 16 | An Innkeeper can reject a Reservation and inform the person who made the reservation. 17 | 18 | [Watch it here!](./assets/onboard02-denied.mp4) 19 | 20 | ### Make Teanant an Issuer 21 | Once the Tenant has accessed the system, they can make their Tenant an Issuer by connecting to an Endorser and Registering a Public DID 22 | 23 | [Watch it here!](./assets/onboard03-issuer.mp4) -------------------------------------------------------------------------------- /docs/assets/onboard01-reservation.mp4: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:594e29d0f8f294841bc8abec1a59dc230bd23d4368bc3aba02a00c0936237328 3 | size 20734903 4 | -------------------------------------------------------------------------------- /docs/assets/onboard02-denied.mp4: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d2751bce94f315721e53689593d4bdfbded1a42fe288c81352f60659efce0487 3 | size 3920802 4 | -------------------------------------------------------------------------------- /docs/assets/onboard03-issuer.mp4: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2bd2bcdbc2565947a98357fc8e5200fae686a3ab639940b422dadf375f172213 3 | size 5230564 4 | -------------------------------------------------------------------------------- /docs/assets/readme-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/assets/readme-logo.png -------------------------------------------------------------------------------- /docs/assets/traction-flow-chart-1600x900-12162022-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/assets/traction-flow-chart-1600x900-12162022-01.jpg -------------------------------------------------------------------------------- /docs/assets/traction-flow-chart-1600x900-12162022.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/assets/traction-flow-chart-1600x900-12162022.pdf -------------------------------------------------------------------------------- /docs/tenant-ui/EMDT_001.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/tenant-ui/EMDT_001.xd -------------------------------------------------------------------------------- /docs/tenant-ui/EMDT_002.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/tenant-ui/EMDT_002.xd -------------------------------------------------------------------------------- /docs/tenant-ui/EMDT_003.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/tenant-ui/EMDT_003.xd -------------------------------------------------------------------------------- /docs/tenant-ui/EMDT_004_Presentation.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/tenant-ui/EMDT_004_Presentation.xd -------------------------------------------------------------------------------- /docs/tenant-ui/EMDT_Chat_001.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/tenant-ui/EMDT_Chat_001.xd -------------------------------------------------------------------------------- /docs/tenant-ui/Email_templates_001.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/tenant-ui/Email_templates_001.xd -------------------------------------------------------------------------------- /docs/tenant-ui/InnKeeper_001.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/tenant-ui/InnKeeper_001.xd -------------------------------------------------------------------------------- /docs/tenant-ui/Messages_001.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/tenant-ui/Messages_001.xd -------------------------------------------------------------------------------- /docs/tenant-ui/QR_Code_001.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/tenant-ui/QR_Code_001.xd -------------------------------------------------------------------------------- /docs/tenant-ui/README.md: -------------------------------------------------------------------------------- 1 | # Documents for the Tenant-UI Service 2 | -------------------------------------------------------------------------------- /docs/tenant-ui/Tenant_SelfServe_001.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/tenant-ui/Tenant_SelfServe_001.xd -------------------------------------------------------------------------------- /docs/tenant-ui/persona_001.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/tenant-ui/persona_001.xd -------------------------------------------------------------------------------- /docs/tenant-ui/toast_messages.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/docs/tenant-ui/toast_messages.xd -------------------------------------------------------------------------------- /docs/traction-anoncreds-workshop.md: -------------------------------------------------------------------------------- 1 | # A Traction Sandbox ACA-Py/AnonCreds Workshop 2 | 3 | Find the Traction AnonCreds workshop in its new home at [ACA-Py Docs](https://aca-py.org/latest/demo/ACA-Py-Workshop/). 4 | -------------------------------------------------------------------------------- /lintconf.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | rules: 3 | braces: 4 | min-spaces-inside: 0 5 | max-spaces-inside: 0 6 | min-spaces-inside-empty: -1 7 | max-spaces-inside-empty: -1 8 | brackets: 9 | min-spaces-inside: 0 10 | max-spaces-inside: 0 11 | min-spaces-inside-empty: -1 12 | max-spaces-inside-empty: -1 13 | colons: 14 | max-spaces-before: 0 15 | max-spaces-after: 1 16 | commas: 17 | max-spaces-before: 0 18 | min-spaces-after: 1 19 | max-spaces-after: 1 20 | comments: 21 | require-starting-space: true 22 | min-spaces-from-content: 2 23 | document-end: disable 24 | document-start: disable # No --- to start a file 25 | empty-lines: 26 | max: 2 27 | max-start: 0 28 | max-end: 0 29 | hyphens: 30 | max-spaces-after: 1 31 | indentation: 32 | spaces: consistent 33 | indent-sequences: whatever # - list indentation will handle both indentation and without 34 | check-multi-line-strings: false 35 | key-duplicates: enable 36 | line-length: disable # Lines can be any length 37 | new-line-at-end-of-file: disable 38 | new-lines: 39 | type: unix 40 | trailing-spaces: enable 41 | truthy: 42 | level: warning -------------------------------------------------------------------------------- /plugins/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VARIANT="3.12" 2 | FROM mcr.microsoft.com/devcontainers/python:${VARIANT} 3 | 4 | ARG POETRY_VERSION="1.8.3" 5 | ENV POETRY_HOME="/opt/poetry" \ 6 | POETRY_VERSION=${POETRY_VERSION} 7 | 8 | RUN curl -sSL https://install.python-poetry.org | python3 - \ 9 | && update-alternatives --install /usr/local/bin/poetry poetry /opt/poetry/bin/poetry 900 \ 10 | # Enable tab completion for bash 11 | && poetry completions bash >> /home/vscode/.bash_completion \ 12 | # Enable tab completion for Zsh 13 | && mkdir -p /home/vscode/.zfunc/ \ 14 | && poetry completions zsh > /home/vscode/.zfunc/_poetry \ 15 | && echo "fpath+=~/.zfunc\nautoload -Uz compinit && compinit" >> /home/vscode/.zshrc 16 | 17 | COPY pyproject.toml poetry.lock ./ 18 | COPY /traction_innkeeper ./traction_innkeeper 19 | RUN poetry config virtualenvs.create false \ 20 | && poetry install --no-root --all-extras \ 21 | && rm -rf /root/.cache/pypoetry -------------------------------------------------------------------------------- /plugins/.devcontainer/docker-compose.devcontainer.yml: -------------------------------------------------------------------------------- 1 | services: 2 | traction-db: 3 | image: "postgres:12" 4 | environment: 5 | - POSTGRES_PASSWORD=postgresPass 6 | ports: 7 | - 5432:5432 8 | volumes: 9 | - traction-wallet:/var/lib/postgresql/data 10 | healthcheck: 11 | test: ["CMD-SHELL", "pg_isready -U postgres"] 12 | interval: 10s 13 | timeout: 5s 14 | retries: 5 15 | 16 | tenant-proxy: 17 | build: 18 | context: .. 19 | dockerfile: ./docker/Dockerfile.tenant-proxy 20 | restart: unless-stopped 21 | environment: 22 | - ACAPY_ADMIN_URL=http://host.docker.internal:3001 23 | - ACAPY_ADMIN_URL_API_KEY=change-me 24 | ports: 25 | - 8032:8080 26 | extra_hosts: 27 | - host.docker.internal:host-gateway 28 | 29 | volumes: 30 | traction-wallet: -------------------------------------------------------------------------------- /plugins/.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | traction-agent: 3 | build: 4 | context: .. 5 | dockerfile: ./docker/Dockerfile 6 | depends_on: 7 | traction-db: 8 | condition: service_healthy 9 | ports: 10 | - 3001:3001 11 | - 3000:3000 12 | - 3002:3002 13 | volumes: 14 | - ./configs:/home/aries/configs:ro,z 15 | extra_hosts: 16 | - host.docker.internal:host-gateway 17 | entrypoint: > 18 | /bin/bash -c 'aca-py "$$@"' -- 19 | command: > 20 | start --arg-file configs/default.yml 21 | 22 | traction-db: 23 | image: "postgres:12" 24 | environment: 25 | - POSTGRES_PASSWORD=postgresPass 26 | ports: 27 | - 5432:5432 28 | volumes: 29 | - traction-wallet:/var/lib/postgresql/data 30 | healthcheck: 31 | test: ["CMD-SHELL", "pg_isready -U postgres"] 32 | interval: 10s 33 | timeout: 5s 34 | retries: 5 35 | 36 | tenant-proxy: 37 | build: 38 | context: .. 39 | dockerfile: ./docker/Dockerfile.tenant-proxy 40 | restart: unless-stopped 41 | environment: 42 | - ACAPY_ADMIN_URL=http://traction-agent:3001 43 | - ACAPY_ADMIN_URL_API_KEY=change-me 44 | ports: 45 | - 8032:8080 46 | extra_hosts: 47 | - host.docker.internal:host-gateway 48 | 49 | volumes: 50 | traction-wallet: -------------------------------------------------------------------------------- /plugins/.devcontainer/post-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | # Convenience workspace directory for later use 5 | WORKSPACE_DIR=$(pwd) 6 | 7 | # install all ACA-Py requirements 8 | python -m pip install --upgrade pip -------------------------------------------------------------------------------- /plugins/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .venv -------------------------------------------------------------------------------- /plugins/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Run/Debug Plugin", 9 | "type": "python", 10 | "request": "launch", 11 | "preLaunchTask": "docker-compose-start", 12 | "module": "poetry", 13 | "justMyCode": false, 14 | "args": [ 15 | "run", 16 | "python", 17 | "-m", 18 | "aries_cloudagent", 19 | "start", 20 | "--arg-file=${workspaceRoot}/.devcontainer/configs/devcontainer.yml" 21 | ] 22 | }] 23 | } -------------------------------------------------------------------------------- /plugins/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "docker-compose-start", 8 | "type": "shell", 9 | "command": "docker compose -f ${workspaceRoot}/.devcontainer/docker-compose.devcontainer.yml up --build -d", 10 | "isBackground": true, 11 | "problemMatcher": [ 12 | { 13 | "pattern": [{ "regexp": ".", "file": 1, "location": 2, "message": 3, }], 14 | "background": { 15 | "activeOnStart": true, 16 | "beginsPattern": "^Container demo-tenant-proxy-1$", 17 | "endsPattern": "^Container demo-traction-db-1$", 18 | } 19 | }, 20 | ] 21 | }, 22 | ] 23 | } -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Traction Aca-Py Plugins 2 | 3 | Traction is a set of plugins that facilitate multi-tenant management within [Aca-Py](https://github.com/openwallet-foundation/acapy). Some of the plugins ([basicmessage_storage](./basicmessage_storage/README.md), [connection_update](./connection_update/README.md) and [multitenant_provider](./multitenant_provider/README.md)) can be used independently from Traction. 4 | 5 | 6 | ### Developing Aca-Py Plugins 7 | 8 | Please refer to [Features: Plugins](https://github.com/openwallet-foundation/acapy/blob/main/docs/features/PlugIns.md) for an in depth look at how to build Aca-Py plugins and how they operate within Aca-Py. 9 | -------------------------------------------------------------------------------- /plugins/docker/Dockerfile.tenant-proxy: -------------------------------------------------------------------------------- 1 | FROM nginxinc/nginx-unprivileged:stable-alpine-slim 2 | COPY docker/tenant-proxy.conf.template /etc/nginx/templates/default.conf.template -------------------------------------------------------------------------------- /plugins/docker/README.md: -------------------------------------------------------------------------------- 1 | This will contain the dockerfile that we will build our acapy + plugins image 2 | 3 | Lots to do here to clean up where the plugin code comes from etc... 4 | This uses the plugins/pyproject.toml to specify a new project that includes the (local) plugins as dependencies, should probably pull in versioned ones. 5 | 6 | The dockerfile is copying over the local plugins code, but should it? Probably should be pulling in versioned code. 7 | 8 | ### developer notes 9 | 10 | - install python 3.12 11 | - install poetry version 2.1.1 12 | 13 | ### build and run 14 | ``` 15 | cd docker 16 | docker build -f ./Dockerfile --tag traction_plugins .. 17 | docker run -it -p 3000:3000 -p 3001:3001 --rm traction_plugins 18 | ``` 19 | -------------------------------------------------------------------------------- /plugins/traction_innkeeper/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim AS base 2 | WORKDIR /usr/src/app 3 | 4 | # Install and configure poetry 5 | USER root 6 | 7 | ENV POETRY_VERSION=2.1.1 8 | ENV POETRY_HOME=/opt/poetry 9 | RUN apt-get update && apt-get install -y curl && apt-get clean 10 | RUN curl -sSL https://install.python-poetry.org | python - 11 | 12 | ENV PATH="/opt/poetry/bin:$PATH" 13 | RUN poetry config virtualenvs.in-project true 14 | 15 | # Setup project 16 | RUN mkdir traction_innkeeper && touch traction_innkeeper/__init__.py 17 | COPY pyproject.toml poetry.lock README.md ./ 18 | ARG install_flags=--no-dev 19 | RUN poetry install ${install_flags} 20 | USER $user 21 | 22 | FROM python:3.12-bullseye 23 | WORKDIR /usr/src/app 24 | COPY --from=base /usr/src/app/.venv /usr/src/app/.venv 25 | ENV PATH="/usr/src/app/.venv/bin:$PATH" 26 | 27 | COPY traction_innkeeper/ traction_innkeeper/ 28 | COPY docker/default.yml ./ 29 | 30 | ENTRYPOINT ["/bin/bash", "-c", "aca-py \"$@\"", "--"] 31 | CMD ["start", "--arg-file", "default.yml"] -------------------------------------------------------------------------------- /plugins/traction_innkeeper/docker/default.yml: -------------------------------------------------------------------------------- 1 | label: traction-innkeeper-plugin 2 | 3 | # Admin 4 | admin: [0.0.0.0, 3001] 5 | admin-insecure-mode: false 6 | admin-api-key: change-me 7 | 8 | # Transport 9 | inbound-transport: 10 | - [http, 0.0.0.0, 3000] 11 | - [ws, 0.0.0.0, 3002] 12 | outbound-transport: http 13 | endpoint: 14 | - http://localhost:3000 15 | 16 | # plugins 17 | plugin: 18 | - traction_innkeeper.v1_0 19 | 20 | plugin-config-value: 21 | - traction_innkeeper.innkeeper_wallet.tenant_id=innkeeper 22 | - traction_innkeeper.innkeeper_wallet.wallet_name=traction_innkeeper 23 | - traction_innkeeper.innkeeper_wallet.wallet_key=change-me 24 | - traction_innkeeper.innkeeper_wallet.print_key=true 25 | - traction_innkeeper.innkeeper_wallet.print_token=true 26 | - traction_innkeeper.reservation.expiry_minutes=2880 27 | 28 | #config 29 | genesis-url: https://indy.igrant.io/genesis 30 | 31 | # Multi-tenancy 32 | multitenant: true 33 | jwt-secret: insecure-jwt-secret 34 | multitenant-admin: true 35 | 36 | # Wallet 37 | # wallet-name: default 38 | # wallet-type: indy 39 | # wallet-key: "insecure, for use in demo only" 40 | # auto-provision: true 41 | 42 | log-level: info -------------------------------------------------------------------------------- /plugins/traction_innkeeper/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # https://github.com/ambv/black#line-length 3 | max-line-length = 90 4 | exclude = 5 | */tests/** 6 | extend_ignore = D202, W503, E203 7 | per_file_ignores = */__init__.py:D104 8 | 9 | [tool:pytest] 10 | addopts = -p no:warnings -------------------------------------------------------------------------------- /plugins/traction_innkeeper/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | 5 | if __name__ == "__main__": 6 | setup() 7 | -------------------------------------------------------------------------------- /plugins/traction_innkeeper/traction_innkeeper/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/plugins/traction_innkeeper/traction_innkeeper/__init__.py -------------------------------------------------------------------------------- /plugins/traction_innkeeper/traction_innkeeper/definition.py: -------------------------------------------------------------------------------- 1 | """Version definitions for this plugin.""" 2 | 3 | versions = [ 4 | { 5 | "major_version": 1, 6 | "minimum_minor_version": 0, 7 | "current_minor_version": 0, 8 | "path": "v1_0", 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /plugins/traction_innkeeper/traction_innkeeper/v1_0/connections/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from acapy_agent.config.injection_context import InjectionContext 4 | from acapy_agent.core.event_bus import EventBus 5 | from acapy_agent.core.plugin_registry import PluginRegistry 6 | from acapy_agent.core.protocol_registry import ProtocolRegistry 7 | 8 | 9 | LOGGER = logging.getLogger(__name__) 10 | 11 | 12 | async def setup(context: InjectionContext): 13 | LOGGER.info("> plugin setup...") 14 | 15 | protocol_registry = context.inject(ProtocolRegistry) 16 | if not protocol_registry: 17 | raise ValueError("ProtocolRegistry missing in context") 18 | 19 | plugin_registry = context.inject(PluginRegistry) 20 | if not plugin_registry: 21 | raise ValueError("PluginRegistry missing in context") 22 | 23 | bus = context.inject(EventBus) 24 | if not bus: 25 | raise ValueError("EventBus missing in context") 26 | 27 | LOGGER.info("< plugin setup.") 28 | -------------------------------------------------------------------------------- /plugins/traction_innkeeper/traction_innkeeper/v1_0/creddef_storage/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from acapy_agent.config.injection_context import InjectionContext 4 | from acapy_agent.core.event_bus import EventBus 5 | from acapy_agent.core.plugin_registry import PluginRegistry 6 | 7 | from acapy_agent.core.protocol_registry import ProtocolRegistry 8 | 9 | from .creddef_storage_service import CredDefStorageService, subscribe 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | async def setup(context: InjectionContext): 15 | LOGGER.info("> plugin setup...") 16 | 17 | protocol_registry = context.inject(ProtocolRegistry) 18 | if not protocol_registry: 19 | raise ValueError("ProtocolRegistry missing in context") 20 | 21 | plugin_registry = context.inject(PluginRegistry) 22 | if not plugin_registry: 23 | raise ValueError("PluginRegistry missing in context") 24 | 25 | bus = context.inject(EventBus) 26 | if not bus: 27 | raise ValueError("EventBus missing in context") 28 | 29 | srv = CredDefStorageService() 30 | context.injector.bind_instance(CredDefStorageService, srv) 31 | 32 | subscribe(bus) 33 | 34 | LOGGER.info("< plugin setup.") 35 | -------------------------------------------------------------------------------- /plugins/traction_innkeeper/traction_innkeeper/v1_0/endorser/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from acapy_agent.config.injection_context import InjectionContext 4 | from acapy_agent.core.event_bus import EventBus 5 | from acapy_agent.core.plugin_registry import PluginRegistry 6 | from acapy_agent.core.protocol_registry import ProtocolRegistry 7 | 8 | from .endorser_connection_service import ( 9 | EndorserConnectionService, 10 | subscribe, 11 | ) 12 | 13 | LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | async def setup(context: InjectionContext): 17 | LOGGER.info("> plugin setup...") 18 | 19 | protocol_registry = context.inject(ProtocolRegistry) 20 | if not protocol_registry: 21 | raise ValueError("ProtocolRegistry missing in context") 22 | 23 | plugin_registry = context.inject(PluginRegistry) 24 | if not plugin_registry: 25 | raise ValueError("PluginRegistry missing in context") 26 | 27 | bus = context.inject(EventBus) 28 | if not bus: 29 | raise ValueError("EventBus missing in context") 30 | 31 | srv = EndorserConnectionService() 32 | context.injector.bind_instance(EndorserConnectionService, srv) 33 | 34 | subscribe(bus) 35 | 36 | LOGGER.info("< plugin setup.") 37 | -------------------------------------------------------------------------------- /plugins/traction_innkeeper/traction_innkeeper/v1_0/schema_storage/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from acapy_agent.config.injection_context import InjectionContext 4 | from acapy_agent.core.event_bus import EventBus 5 | from acapy_agent.core.plugin_registry import PluginRegistry 6 | from acapy_agent.core.protocol_registry import ProtocolRegistry 7 | 8 | from .schema_storage_service import SchemaStorageService, subscribe 9 | 10 | LOGGER = logging.getLogger(__name__) 11 | 12 | 13 | async def setup(context: InjectionContext): 14 | LOGGER.info("> plugin setup...") 15 | 16 | protocol_registry = context.inject(ProtocolRegistry) 17 | if not protocol_registry: 18 | raise ValueError("ProtocolRegistry missing in context") 19 | 20 | plugin_registry = context.inject(PluginRegistry) 21 | if not plugin_registry: 22 | raise ValueError("PluginRegistry missing in context") 23 | 24 | bus = context.inject(EventBus) 25 | if not bus: 26 | raise ValueError("EventBus missing in context") 27 | 28 | srv = SchemaStorageService() 29 | context.injector.bind_instance(SchemaStorageService, srv) 30 | 31 | subscribe(bus) 32 | 33 | LOGGER.info("< plugin setup.") 34 | -------------------------------------------------------------------------------- /scripts/endorser-acapy-args.yml: -------------------------------------------------------------------------------- 1 | auto-accept-invites: true 2 | auto-accept-requests: true 3 | auto-respond-messages: true 4 | auto-ping-connection: true 5 | auto-provision: true 6 | monitor-ping: true 7 | public-invites: true 8 | outbound-transport: http 9 | log-level: info 10 | endorser-protocol-role: endorser 11 | requests-through-public-did: true 12 | #endorser-public-did: 13 | #endorser-alias: 14 | #auto-request-endorsement: 15 | auto-endorse-transactions: true 16 | #auto-write-transactions: 17 | #auto-create-revocation-transactions: 18 | -------------------------------------------------------------------------------- /scripts/ledgers.yml: -------------------------------------------------------------------------------- 1 | - id: bcovrin-test 2 | is_production: true 3 | is_write: true 4 | genesis_url: 'http://test.bcovrin.vonx.io/genesis' 5 | endorser_did: 'SVfHGCEEvEFmpBPcxgNqRR' 6 | endorser_alias: 'bcovrin-test-endorser' 7 | - id: bcovrin-test-1 8 | is_production: true 9 | is_write: true 10 | genesis_url: 'http://test.bcovrin.vonx.io/genesis' 11 | endorser_did: 'SVfHGCEEvEFmpBPcxgNqRR' 12 | endorser_alias: 'bcovrin-test-endorser-1' -------------------------------------------------------------------------------- /scripts/plugin-config.yml: -------------------------------------------------------------------------------- 1 | multitenant_provider: 2 | manager: 3 | class_name: "multitenant_provider.v1_0.manager.AskarMultitokenMultitenantManager" 4 | always_check_provided_wallet_key: true 5 | errors: 6 | on_unneeded_wallet_key: false 7 | token_expiry: 8 | units: days 9 | amount: 1 10 | 11 | traction_innkeeper: 12 | innkeeper_wallet: 13 | tenant_id: innkeeper 14 | wallet_name: traction_innkeeper 15 | wallet_key: change-me 16 | print_key: true 17 | print_token: true 18 | connect_to_endorser: [ 19 | { 20 | "endorser_alias": "bcovrin-test-endorser", 21 | "ledger_id": "bcovrin-test", 22 | }, 23 | { 24 | "endorser_alias": "bcovrin-test-endorser-1", 25 | "ledger_id": "bcovrin-test-1", 26 | } 27 | ] 28 | create_public_did: ["bcovrin-test", "bcovrin-test-1"] 29 | reservation: 30 | auto_approve: true 31 | expiry_minutes: 2880 32 | auto_issuer: true 33 | 34 | basicmessage_storage: 35 | wallet_enabled: true 36 | -------------------------------------------------------------------------------- /services/aca-py/Dockerfile.acapy: -------------------------------------------------------------------------------- 1 | FROM traction:plugins-acapy 2 | 3 | USER root 4 | 5 | ADD https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 ./jq 6 | RUN chmod +x ./jq 7 | COPY ngrok-wait.sh ngrok-wait.sh 8 | RUN chmod +x ./ngrok-wait.sh 9 | -------------------------------------------------------------------------------- /services/tenant-ui/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tenant-ui", 3 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 4 | "image": "mcr.microsoft.com/devcontainers/typescript-node:0-20", 5 | "features": { 6 | "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, 7 | "ghcr.io/devcontainers-contrib/features/vue-cli:2": {} 8 | }, 9 | "customizations": { 10 | "vscode": { 11 | "extensions": [ 12 | "Vue.volar", 13 | "esbenp.prettier-vscode" 14 | ] 15 | } 16 | }, 17 | 18 | // Features to add to the dev container. More info: https://containers.dev/features. 19 | // "features": {}, 20 | 21 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 22 | "forwardPorts": [ 23 | 8080 24 | ], 25 | 26 | // Use 'postCreateCommand' to run commands after the container is created. 27 | "postCreateCommand": "bash ./.devcontainer/post-install.sh" 28 | 29 | // Configure tool-specific properties. 30 | // "customizations": {}, 31 | 32 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 33 | // "remoteUser": "root", 34 | 35 | // Enable this on OSX to add ssh key to agent inside container 36 | // "initializeCommand": "find ~/.ssh/ -type f -exec grep -l 'PRIVATE' {} \\; | xargs ssh-add" 37 | } 38 | -------------------------------------------------------------------------------- /services/tenant-ui/.devcontainer/post-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | # Convenience workspace directory for later use 5 | WORKSPACE_DIR=$(pwd) 6 | # this might be too slow for each start up? 7 | rm -rf dist && rm -rf node_modules && rm -rf frontend/dist && rm -rf frontend/node_modules 8 | npm install && npm cache clean --force && npm install -g typescript && cd frontend && npm install && npm cache clean --force && cd .. && npm run build -------------------------------------------------------------------------------- /services/tenant-ui/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | venv/ 3 | .env/ 4 | dist/ 5 | node_modules/ 6 | frontend/venv/ 7 | frontend/.env/ 8 | frontend/dist/ 9 | frontend/node_modules/ 10 | config/local.json 11 | .gitignore 12 | -------------------------------------------------------------------------------- /services/tenant-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Credentials 2 | .env 3 | 4 | # Configurations 5 | .venv 6 | !.vscode/* 7 | .vscode/* 8 | !.vscode/extensions.json 9 | !.vscode/launch.json 10 | !.vscode/settings.json 11 | .idea 12 | config/local.json 13 | 14 | # Temporary files 15 | *.log 16 | *.swp 17 | *.zip 18 | *.tgz 19 | schema.ts 20 | 21 | # Temporary Code 22 | node_modules 23 | dist 24 | 25 | # Test data 26 | frontend/public/test_schemas.json 27 | coverage/* 28 | 29 | # helper scripts 30 | .utils 31 | -------------------------------------------------------------------------------- /services/tenant-ui/.prettierignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build outputs 5 | dist/ 6 | build/ 7 | coverage/ 8 | 9 | # Other parts of the project 10 | frontend/ 11 | config/ 12 | .devcontainer/ 13 | README.md 14 | 15 | # Package files 16 | package-lock.json 17 | yarn.lock 18 | pnpm-lock.yaml 19 | tsconfig.json 20 | 21 | # Logs 22 | *.log 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Generated files 28 | *.d.ts.map 29 | *.js.map 30 | 31 | # IDE files 32 | .vscode/ 33 | .idea/ 34 | 35 | # OS files 36 | .DS_Store 37 | Thumbs.db 38 | -------------------------------------------------------------------------------- /services/tenant-ui/.prettierrc: -------------------------------------------------------------------------------- 1 | trailingComma: "es5" 2 | semi: true 3 | singleQuote: false 4 | printWidth: 80 5 | bracketSpacing: true 6 | -------------------------------------------------------------------------------- /services/tenant-ui/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vue.volar", 4 | "esbenp.prettier-vscode" 5 | ] 6 | } -------------------------------------------------------------------------------- /services/tenant-ui/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "backend - run dev", 9 | "request": "launch", 10 | "runtimeArgs": ["run-script", "dev"], 11 | "runtimeExecutable": "npm", 12 | "skipFiles": [], 13 | "type": "node", 14 | "env": {} 15 | }, 16 | { 17 | "name": "frontend - run dev", 18 | "request": "launch", 19 | "cwd": "${workspaceFolder}/frontend", 20 | "runtimeArgs": ["run-script", "dev", "--", "--host"], 21 | "runtimeExecutable": "npm", 22 | "skipFiles": [], 23 | "type": "node", 24 | "env": {} 25 | }, 26 | { 27 | "name": "frontend - chrome", 28 | "type": "chrome", 29 | "request": "launch", 30 | "url": "http://localhost:5173/", 31 | "webRoot": "${workspaceFolder}/frontend", 32 | "breakOnLoad": true, 33 | "sourceMapPathOverrides": { 34 | "webpack:///./src/*": "${webRoot}/*" 35 | } 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /services/tenant-ui/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | "prettier.enable": true, 7 | "[javascript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[typescript]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[vue]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "files.associations": { 17 | ".prettierrc": "yaml" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /services/tenant-ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:24-alpine 2 | 3 | WORKDIR /usr/src/app 4 | COPY . . 5 | # install backend libs 6 | RUN npm cache clean --force && npm ci 7 | # install frontend libs 8 | RUN cd frontend && npm cache clean --force && npm ci 9 | # build backend 10 | RUN npm run build 11 | # build frontend 12 | RUN cd frontend && npm run build 13 | # these are not needed now, save some space 14 | RUN rm -rf frontend/node_modules 15 | 16 | EXPOSE 8080 17 | CMD [ "node", "--enable-source-maps", "dist/src/index.js" ] -------------------------------------------------------------------------------- /services/tenant-ui/config/forms/reservation.json: -------------------------------------------------------------------------------- 1 | { 2 | "formDataSchema": { 3 | "type": "object", 4 | "properties": { 5 | "organization": { 6 | "type": "string" 7 | } 8 | } 9 | }, 10 | "formUISchema": { 11 | "type": "VerticalLayout", 12 | "elements": [ 13 | { 14 | "type": "Control", 15 | "scope": "#/properties/organization", 16 | "label": "Company/organization " 17 | } 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /services/tenant-ui/frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | # Change these settings to your own preference 9 | indent_style = space 10 | indent_size = 2 11 | 12 | # I recommend you to keep these unchanged 13 | end_of_line = lf 14 | charset = utf-8 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "plugin:vue/vue3-recommended", 10 | "eslint:recommended", 11 | "@vue/typescript/recommended", 12 | "@vue/prettier", 13 | "@vue/eslint-config-prettier", 14 | "plugin:@intlify/vue-i18n/recommended" 15 | ], 16 | "parserOptions": { 17 | "ecmaVersion": 2021 18 | }, 19 | "plugins": [], 20 | "rules": { 21 | "@typescript-eslint/no-explicit-any": "off", 22 | "@typescript-eslint/ban-types": "off", 23 | "@typescript-eslint/no-unused-vars": "off", 24 | "@typescript-eslint/no-empty-function": "off", 25 | "vue/multi-word-component-names": "off", 26 | "@intlify/vue-i18n/no-missing-keys": "error", 27 | "@intlify/vue-i18n/no-unused-keys": "warn", 28 | "@intlify/vue-i18n/no-raw-text": [ 29 | "error", 30 | { 31 | "extension": [ 32 | ".ts", 33 | ".vue" 34 | ] 35 | } 36 | ] 37 | }, 38 | "settings": { 39 | "vue-i18n": { 40 | "localeDir": "./src/locales/*.json", 41 | "messageSyntaxVersion": "^9.2.2" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /services/tenant-ui/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | coverage 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? -------------------------------------------------------------------------------- /services/tenant-ui/frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.css 2 | **/*.scss -------------------------------------------------------------------------------- /services/tenant-ui/frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | trailingComma: 'es5' 2 | semi: true 3 | singleQuote: true 4 | printWidth: 80 5 | bracketSpacing: true 6 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Loading... 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/public/forms/reservation.json: -------------------------------------------------------------------------------- 1 | { 2 | "formDataSchema": { 3 | "type": "object", 4 | "properties": { 5 | "fullName": { 6 | "type": "string" 7 | }, 8 | "phoneNumber": { 9 | "type": "string" 10 | }, 11 | "tenantReason": { 12 | "type": "string" 13 | } 14 | }, 15 | "required": [] 16 | }, 17 | "formUISchema": { 18 | "type": "VerticalLayout", 19 | "elements": [ 20 | { 21 | "type": "Control", 22 | "scope": "#/properties/phoneNumber", 23 | "label": "Phone / Mobile" 24 | }, 25 | { 26 | "type": "Control", 27 | "scope": "#/properties/fullName" 28 | }, 29 | { 30 | "type": "Control", 31 | "scope": "#/properties/tenantReason", 32 | "options": { 33 | "multi": true 34 | } 35 | } 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/public/img/badges/issuer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/services/tenant-ui/frontend/public/img/badges/issuer.png -------------------------------------------------------------------------------- /services/tenant-ui/frontend/public/img/badges/issuer_shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/services/tenant-ui/frontend/public/img/badges/issuer_shield.png -------------------------------------------------------------------------------- /services/tenant-ui/frontend/public/img/badges/wallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/services/tenant-ui/frontend/public/img/badges/wallet.png -------------------------------------------------------------------------------- /services/tenant-ui/frontend/public/img/bc/bc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/services/tenant-ui/frontend/public/img/bc/bc_logo.png -------------------------------------------------------------------------------- /services/tenant-ui/frontend/public/img/default-login-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/services/tenant-ui/frontend/public/img/default-login-image.jpg -------------------------------------------------------------------------------- /services/tenant-ui/frontend/public/img/innkeeper/innkeeper-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/services/tenant-ui/frontend/public/img/innkeeper/innkeeper-badge.png -------------------------------------------------------------------------------- /services/tenant-ui/frontend/public/img/innkeeper/innkeeper-login-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/services/tenant-ui/frontend/public/img/innkeeper/innkeeper-login-image.jpg -------------------------------------------------------------------------------- /services/tenant-ui/frontend/public/img/logo/aries-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/traction/6f4b9f60895eaabf1af75a7fd31b39c028932b34/services/tenant-ui/frontend/public/img/logo/aries-logo-color.png -------------------------------------------------------------------------------- /services/tenant-ui/frontend/public/img/logo/traction-logo-5.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/public/img/logo/traction-logo-7.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/scripts/i18n/commonKeys.js: -------------------------------------------------------------------------------- 1 | import { resolve, dirname, basename } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import { globSync } from 'glob'; 4 | import { createRequire } from 'module'; 5 | import { flattenLocales } from './utils.js'; 6 | 7 | const require = createRequire(import.meta.url); 8 | 9 | const modulePaths = globSync( 10 | resolve( 11 | dirname(fileURLToPath(import.meta.url)), 12 | '../../src/plugins/i18n/locales/*.json' 13 | ) 14 | ); 15 | 16 | const locales = modulePaths.reduce( 17 | (acc, path) => ({ 18 | ...acc, 19 | [basename(path).replace('.json', '')]: require(resolve(path)), 20 | }), 21 | {} 22 | ); 23 | 24 | // Flatten all locales. 25 | const flattened = flattenLocales(locales); 26 | 27 | // List all values in the flattened 'en' locale that are duplicated and their keys. 28 | const valuesByKey = Object.entries(flattened.en).reduce( 29 | (acc, [key, value]) => ({ 30 | ...acc, 31 | [value]: [...(acc[value] || []), key], 32 | }), 33 | {} 34 | ); 35 | 36 | const duplicates = Object.entries(valuesByKey) 37 | .filter(([_, keys]) => keys.length > 1) 38 | .reduce( 39 | (acc, [value, keys]) => ({ 40 | ...acc, 41 | [value]: keys, 42 | }), 43 | {} 44 | ); 45 | 46 | console.log('Duplicate i18n values:', duplicates); 47 | console.log( 48 | 'Consider adding these to a common object:', 49 | Object.keys(duplicates) 50 | ); 51 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/scripts/i18n/utils.js: -------------------------------------------------------------------------------- 1 | import { flatten, unflatten } from 'flat'; 2 | 3 | // Flatten unflattened locales. 4 | export const flattenLocales = (unflattened) => { 5 | return Object.entries(unflattened).reduce( 6 | (acc, [locale, data]) => ({ 7 | ...acc, 8 | [locale]: flatten(data, { object: true }), 9 | }), 10 | {} 11 | ); 12 | }; 13 | 14 | // Unflatten flattened locales. 15 | export const unflattenLocales = (flattened) => { 16 | return Object.entries(flattened).reduce( 17 | (acc, [locale, data]) => ({ 18 | ...acc, 19 | [locale]: unflatten(data, { object: true }), 20 | }), 21 | {} 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/scripts/seed-contacts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | contacts=( 4 | "Ernie" 5 | "Bert" 6 | "Cookie Monster" 7 | "Oscar" 8 | "Scooby Doo" 9 | "Shaggy" 10 | "Velma" 11 | "Daphne" 12 | "Fred" 13 | "Optimus" 14 | "Bambi" 15 | "Marge" 16 | ) 17 | 18 | # Quit unless there is a token 19 | if [ $# -eq 0 ]; then 20 | echo "Please provide a valid token" 21 | exit 1 22 | fi 23 | 24 | for contact in "${contacts[@]}"; do 25 | echo "Creating contact: $contact" 26 | curl -X 'POST' \ 27 | "http://localhost:5100/tenant/v1/contacts/create-invitation" \ 28 | -H "accept: application/json" \ 29 | -H "Authorization: Bearer $1" \ 30 | -H "Content-Type: application/json" \ 31 | -d "{ \"alias\":\"$contact\", \"invitation_type\": \"connections/1.0\" }" 32 | sleep 0.5 # Throttle requests 33 | done -------------------------------------------------------------------------------- /services/tenant-ui/frontend/scripts/seed-schemas.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Quit unless there is a token 4 | if [ $# -eq 0 ]; then 5 | echo "Please provide a valid token" 6 | exit 1 7 | fi 8 | 9 | test_url="https://traction-api-test.apps.silver.devops.gov.bc.ca/tenant/v1/governance/schema_templates" 10 | proxy_url="https://localhost:8080/api/traction/tenant/v1/governance/schema_templates" 11 | 12 | payload=" 13 | { 14 | \"schema_definition\": { 15 | \"schema_name\": \"my_schema\", 16 | \"schema_version\": \"1.0.0\", 17 | \"attributes\": [ 18 | \"my_attribute\" 19 | ] 20 | }, 21 | \"name\": \"my_schema\", 22 | \"tags\": [], 23 | \"credential_definition\": { 24 | \"tag\": \"string\", 25 | \"revocation_enabled\": false, 26 | \"revocation_registry_size\": 0 27 | } 28 | } 29 | " 30 | 31 | curl -X 'POST' \ 32 | url \ 33 | -H "accept: application/json" \ 34 | -H "Authorization: Bearer $1" \ 35 | -H "Content-Type: application/json" \ 36 | -d payload 37 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 38 | 39 | 48 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/assets/primevueOverrides/buttons.scss: -------------------------------------------------------------------------------- 1 | @use '../variables.scss' as *; 2 | 3 | button { 4 | &.p-button { 5 | &:not(.p-button-rounded):not(.p-danger):not(.p-button-text):not(.p-button-link) { 6 | padding: 0.5rem 1rem; 7 | background-color: $tenant-ui-secondary-color; 8 | border-color: $tenant-ui-secondary-color; 9 | &:hover, 10 | &:active { 11 | background-color: $tenant-ui-secondary-active !important; 12 | border-color: $tenant-ui-secondary-color !important; 13 | } 14 | > .p-button-label { 15 | font-weight: normal; 16 | } 17 | } 18 | } 19 | &.p-button-rounded, 20 | &.p-button-text { 21 | color: $tenant-ui-secondary-color !important; 22 | > .p-button-label { 23 | font-weight: normal; 24 | } 25 | } 26 | } 27 | 28 | // Fixing small bug in PrimeVue for input groups 29 | .field .p-inputgroup button.p-button { 30 | border-top-left-radius: 0; 31 | border-bottom-left-radius: 0; 32 | } 33 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/assets/primevueOverrides/formFields.scss: -------------------------------------------------------------------------------- 1 | @use '../variables.scss' as *; 2 | 3 | // Specific to form text fields, layout, spacing, etc 4 | // inputs/textareas etc 5 | 6 | // The generatl field wrapper 7 | .field { 8 | line-height: 24px; 9 | > label { 10 | margin-bottom: 2px !important; 11 | display: block !important; 12 | > .p-progress-spinner { 13 | height: 1em; 14 | width: 1em; 15 | } 16 | } 17 | > small.p-error { 18 | display: block !important; 19 | } 20 | 21 | // Text type inputs 22 | .p-inputtext { 23 | padding: 0.45rem; 24 | border-radius: 5px; 25 | &:hover, 26 | &:focus { 27 | border-color: $tenant-ui-secondary-active !important; 28 | } 29 | &:focus { 30 | box-shadow: 0 0 0.3rem 0.1rem $tenant-ui-shadow !important; 31 | } 32 | } 33 | 34 | // Fixing small bug in PrimeVue for input groups 35 | .p-inputgroup .p-inputtext { 36 | border-top-right-radius: 0; 37 | border-bottom-right-radius: 0; 38 | } 39 | 40 | .p-autocomplete-dd { 41 | input { 42 | border-top-right-radius: 0; 43 | border-bottom-right-radius: 0; 44 | } 45 | button { 46 | border-top-left-radius: 0 !important; 47 | border-bottom-left-radius: 0 !important; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/assets/primevueOverrides/inputSwitch.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | @use '../variables.scss' as *; 3 | 4 | // Input Switches 5 | .p-inputswitch { 6 | &[data-p-highlight="true"] .p-inputswitch-slider { 7 | background: color.adjust($tenant-ui-status-green-background, $lightness: -20%);; 8 | &::before { 9 | background-color: white !important; 10 | } 11 | } 12 | &[data-p-highlight="false"] .p-inputswitch-slider { 13 | background: color.adjust($tenant-ui-status-red-background, $lightness: -20%); 14 | } 15 | &:hover { 16 | &[data-p-highlight="true"] .p-inputswitch-slider { 17 | background: color.adjust($tenant-ui-status-green-background, $lightness: -40%) !important; 18 | } 19 | &[data-p-highlight="false"] .p-inputswitch-slider { 20 | background: color.adjust($tenant-ui-status-red-background, $lightness: -40%) !important; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/assets/primevueOverrides/panels.scss: -------------------------------------------------------------------------------- 1 | @use '../variables.scss' as *; 2 | 3 | // The Accordion and its tabs 4 | .p-accordion { 5 | .p-accordion-tab { 6 | margin-bottom: 0; 7 | &:not(:first-of-type) { 8 | .p-accordion-header-link{ 9 | border-top-right-radius: 0; 10 | border-top-left-radius: 0; 11 | } 12 | } 13 | &:not(:last-of-type) { 14 | .p-accordion-header-link, .p-accordion-content{ 15 | border-bottom-right-radius: 0; 16 | border-bottom-left-radius: 0; 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/assets/primevueOverrides/primevueOverrides.scss: -------------------------------------------------------------------------------- 1 | @forward 'buttons.scss'; 2 | @forward 'datatable.scss'; 3 | @forward 'formFields.scss'; 4 | @forward 'inputSwitch.scss'; 5 | @forward 'panels.scss'; -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/assets/style.scss: -------------------------------------------------------------------------------- 1 | @forward 'primevue/resources/themes/lara-light-blue/theme.css'; // includes '~primevue/resources/themes/nova/theme.css' 2 | @forward 'layout.scss'; 3 | @forward 'tenantuiComponents.scss'; 4 | @forward 'primevueOverrides/primevueOverrides.scss'; 5 | @forward 'toast.scss'; 6 | @forward 'variables.scss'; 7 | @use 'variables.scss' as *; 8 | 9 | // General HTML 10 | html, 11 | body, 12 | #app { 13 | height: 100vh; 14 | padding: 0; 15 | margin: 0; 16 | } 17 | 18 | :root { 19 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 20 | font-size: 14px; 21 | font-weight: 400; 22 | 23 | font-synthesis: none; 24 | text-rendering: optimizeLegibility; 25 | -webkit-font-smoothing: antialiased; 26 | -moz-osx-font-smoothing: grayscale; 27 | -webkit-text-size-adjust: 100%; 28 | } 29 | 30 | h1 { 31 | font-size: 3.2em; 32 | line-height: 1.1; 33 | } 34 | 35 | a { 36 | color: $tenant-ui-link-color; 37 | } 38 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/assets/toast.scss: -------------------------------------------------------------------------------- 1 | @forward "vue-toastification/dist/index.css"; 2 | @use 'variables.scss' as *; 3 | // For overriding the Vue-Toastification library 4 | // https://github.com/Maronato/vue-toastification/blob/main/src/scss/_variables.scss 5 | 6 | // Some custom styling 7 | .Vue-Toastification__toast { 8 | border-left-width: 15px; 9 | border-left-style: solid; 10 | border-radius: 5px; 11 | &.Vue-Toastification__toast--error { 12 | border-left-color: $tenant-ui-status-red-color; 13 | } 14 | &.Vue-Toastification__toast--success { 15 | border-left-color: #569956; 16 | } 17 | &.Vue-Toastification__toast--info { 18 | border-left-color: #04a9f4; 19 | } 20 | &.Vue-Toastification__toast--warning { 21 | border-left-color: $tenant-ui-status-amber-color; 22 | } 23 | .Vue-Toastification__icon { 24 | font-size: 1.5em; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/about/Business.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 26 | 27 | 32 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/about/PluginList.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | 27 | 32 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/authentications/createApiKey/CreateApiKey.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 40 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/common/ConfigItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 33 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/common/InfoModal.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 50 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/common/LoadingLabel.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/common/LocaleSwitcher.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 27 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/common/MenuItemLink.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/common/PanelMenuItemLink.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/common/SessionTimeoutModal.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 27 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/common/SkeletonCard.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/common/StatusChip.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 34 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/connections/acceptInvitation/AcceptInvitation.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 41 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/connections/createConnection/CreateConnection.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 42 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/connections/didExchange/DidExchange.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 42 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/connections/editConnection/EditConnection.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 47 | 48 | 53 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/holder/CredentialAttributes.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 34 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/holder/credentialOcaCard/CardLogo.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 46 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/holder/credentialOcaCard/CardSecondaryBody.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 40 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/holder/credentialOcaCard/CredentialCard10.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 29 | 30 | 45 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/holder/credentialOcaCard/CredentialName.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | 30 | 43 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/holder/credentialOcaCard/IssuerName.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | 30 | 44 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/holder/credentialOcaCard/OcaStyleConstants.ts: -------------------------------------------------------------------------------- 1 | const defaultWidth = 360; 2 | 3 | export const DIMENSIONS = { 4 | width: defaultWidth, 5 | borderRadius: 10, 6 | padding: defaultWidth * 0.05, 7 | logoHeight: defaultWidth * 0.12, 8 | }; 9 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/innkeeper/InnkeeperBadge.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 39 | 40 | 60 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/innkeeper/InnkeeperLoginOidc.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 36 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/innkeeper/authentications/createApiKey/CreateApiKey.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 40 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/issuance/schemas/AddSchemaFromLedger.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 46 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/layout/AppLayout.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 39 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/layout/Footer.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | 15 | 24 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/layout/innkeeper/InnkeeperLayout.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 37 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/layout/mainCard/ExpandButton.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/layout/mainCard/MainCard.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 22 | 35 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/layout/mainCard/MainCardContent.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 29 | 30 | 39 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/messages/createMessage/CreateMessage.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 40 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/notifications/Alert.vue: -------------------------------------------------------------------------------- 1 | 10 | 17 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/oidc/LoginOIDC.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/profile/Developer.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/profile/ProfileFooter.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/reservation/status/CheckedIn.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 29 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/reservation/status/Denied.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 30 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/reservation/status/NotFound.vue: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/reservation/status/Pending.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 35 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/components/reservation/user/OidcUserDisplay.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/composables/useGetItem.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | import { GetItem } from '../types'; 3 | import { fetchItem as utilFetchItem } from '../store/utils/fetchItem'; 4 | 5 | export default function useGetItem(url: string): GetItem { 6 | const data = ref(url); 7 | 8 | const item = ref(); 9 | const loading: any = ref(false); 10 | const error = ref(''); 11 | 12 | async function fetchItem(id?: string, params: any = {}) { 13 | try { 14 | // call store 15 | loading.value = true; 16 | item.value = await utilFetchItem(data.value, id, error, loading, params); 17 | } catch (err: any) { 18 | item.value = null; 19 | error.value = err.message; 20 | } finally { 21 | loading.value = false; 22 | } 23 | } 24 | 25 | return { 26 | item, 27 | loading, 28 | error, 29 | fetchItem, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/helpers/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError } from 'axios'; 2 | import { useToast } from 'vue-toastification'; 3 | 4 | const toast = useToast(); 5 | 6 | const BAD_REQUEST_CODE = 'ERR_BAD_REQUEST'; 7 | 8 | interface ErrorHandler { 9 | error: any | AxiosError; 10 | existsMessage?: string; 11 | badRequestMessage?: string; 12 | internalMessage?: string; 13 | } 14 | 15 | const isDuplicate = (error: any | AxiosError) => 16 | error.response?.data.includes('already exists') || 17 | error.response?.data.includes('Duplicate row'); 18 | 19 | const errorHandler = ({ 20 | error, 21 | existsMessage = 'Duplicate entry', 22 | badRequestMessage = 'Bad request', 23 | internalMessage = 'Internal server error', 24 | }: ErrorHandler) => { 25 | if (axios.isAxiosError(error)) { 26 | if (error.response?.data && isDuplicate(error)) { 27 | toast.error(existsMessage); 28 | return; 29 | } 30 | if (error.code === BAD_REQUEST_CODE) { 31 | toast.error(badRequestMessage); 32 | return; 33 | } 34 | } 35 | 36 | toast.error(`Failure: ${internalMessage}`); 37 | }; 38 | 39 | export default errorHandler; 40 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/helpers/jsonParsing.ts: -------------------------------------------------------------------------------- 1 | export function tryParseJson(jsonString: string): T | undefined { 2 | try { 3 | const o = JSON.parse(jsonString); 4 | if (o && typeof o === 'object') { 5 | return o; 6 | } 7 | return undefined; 8 | } catch (_e) { 9 | return undefined; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/matomoSetup.ts: -------------------------------------------------------------------------------- 1 | const _paq = ((window as any)._paq = (window as any)._paq || []); 2 | _paq.push(['trackPageView']); 3 | _paq.push(['enableLinkTracking']); 4 | function setup(MATOMO_URL: string) { 5 | const u = `//${MATOMO_URL}/`; 6 | _paq.push(['setTrackerUrl', u + 'matomo.php']); 7 | _paq.push(['setSiteId', 1]); 8 | const d = document, 9 | g = d.createElement('script'), 10 | s: any = d.getElementsByTagName('script')[0]; 11 | g.type = 'text/javascript'; 12 | g.async = true; 13 | g.src = u + 'matomo.js'; 14 | s.parentNode.insertBefore(g, s); 15 | } 16 | export { setup }; 17 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/overlayLibrary/interfaces/overlay/BrandingOverlayData.interface.ts: -------------------------------------------------------------------------------- 1 | import { IOverlayData } from './OverlayData.interface'; 2 | 3 | export interface IBrandingOverlayData extends IOverlayData { 4 | logo: string; 5 | background_image: string; 6 | background_image_slice: string; 7 | primary_background_color: string; 8 | secondary_background_color: string; 9 | primary_attribute: string; 10 | secondary_attribute: string; 11 | issued_date_attribute: string; 12 | expiry_date_attribute: string; 13 | } 14 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/overlayLibrary/interfaces/overlay/OverlayBundleData.interface.ts: -------------------------------------------------------------------------------- 1 | import { ICaptureBaseData, IOverlayData } from './OverlayData.interface'; 2 | 3 | export interface IOverlayBundleData { 4 | capture_base: ICaptureBaseData; 5 | overlays: IOverlayData[]; 6 | } 7 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/overlayLibrary/interfaces/overlay/OverlayData.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ICaptureBaseData { 2 | type: string; 3 | classification: string; 4 | attributes: { 5 | [key: string]: string; 6 | }; 7 | flagged_attributes: string[]; 8 | digest?: string; 9 | } 10 | 11 | export interface IOverlayData { 12 | type: string; 13 | capture_base: string; 14 | digest?: string; 15 | } 16 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/overlayLibrary/interfaces/overlay/SemanticOverlay.interface.ts: -------------------------------------------------------------------------------- 1 | import { IOverlayData } from './OverlayData.interface'; 2 | 3 | export interface ICharacterEncodingOverlayData extends IOverlayData { 4 | default_character_encoding: string; 5 | attr_character_encoding: { 6 | [key: string]: string; 7 | }; 8 | } 9 | 10 | export interface IFormatOverlayData extends IOverlayData { 11 | attribute_formats: { 12 | [key: string]: string; 13 | }; 14 | } 15 | 16 | export interface IInformationOverlayData extends IOverlayData { 17 | language: string; 18 | attribute_information: { 19 | [key: string]: string; 20 | }; 21 | } 22 | 23 | export interface ILabelOverlayData extends IOverlayData { 24 | language: string; 25 | attribute_labels: { 26 | [key: string]: string; 27 | }; 28 | attribute_categories: string[]; 29 | category_labels: { 30 | [key: string]: string; 31 | }; 32 | } 33 | 34 | export interface IMetaOverlayData extends IOverlayData { 35 | language: string; 36 | name: string; 37 | description: string; 38 | credential_help_text: string; 39 | credential_support_url: string; 40 | issuer: string; 41 | issuer_description: string; 42 | issuer_url: string; 43 | } 44 | 45 | export interface IStandardOverlayData extends IOverlayData { 46 | attr_standards: { 47 | [key: string]: string; 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/overlayLibrary/services/OverlayBrandingDataFactory.ts: -------------------------------------------------------------------------------- 1 | import { BrandingState } from '../contexts/Branding'; 2 | import { IBrandingOverlayData } from '../interfaces/overlay/BrandingOverlayData.interface'; 3 | 4 | class BrandingOverlayDataFactory { 5 | public static getBrandingOverlayData( 6 | branding: BrandingState 7 | ): IBrandingOverlayData { 8 | const { 9 | captureBase, 10 | type, 11 | digest, 12 | logo, 13 | backgroundImageSlice, 14 | backgroundImage, 15 | primaryBackgroundColor, 16 | secondaryBackgroundColor, 17 | primaryAttribute, 18 | secondaryAttribute, 19 | issuedDateAttribute, 20 | expiryDateAttribute, 21 | } = branding; 22 | return { 23 | capture_base: captureBase ?? '', 24 | type: type ?? '', 25 | digest: digest ?? '', 26 | logo: logo ?? '', 27 | background_image_slice: backgroundImageSlice ?? '', 28 | background_image: backgroundImage ?? '', 29 | primary_background_color: primaryBackgroundColor ?? '', 30 | secondary_background_color: secondaryBackgroundColor ?? '', 31 | primary_attribute: primaryAttribute ?? '', 32 | secondary_attribute: secondaryAttribute ?? '', 33 | issued_date_attribute: issuedDateAttribute ?? '', 34 | expiry_date_attribute: expiryDateAttribute ?? '', 35 | }; 36 | } 37 | } 38 | 39 | export default BrandingOverlayDataFactory; 40 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/overlayLibrary/services/OverlayBundleFactory.ts: -------------------------------------------------------------------------------- 1 | import OverlayBundle from '../types/overlay/OverlayBundle'; 2 | import { IOverlayBundleData } from '../interfaces/overlay/OverlayBundleData.interface'; 3 | 4 | class OverlayBundleFactory { 5 | public static async fetchOverlayBundle( 6 | credentialDefinitionId: string, 7 | url: string 8 | ): Promise { 9 | const response = await fetch(url); 10 | const data: IOverlayBundleData[] = await response.json(); 11 | return this.createOverlayBundle(credentialDefinitionId, data[0]); 12 | } 13 | 14 | public static createOverlayBundle( 15 | id: string, 16 | data: IOverlayBundleData 17 | ): OverlayBundle { 18 | return new OverlayBundle(id, data); 19 | } 20 | } 21 | 22 | export default OverlayBundleFactory; 23 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/overlayLibrary/types/overlay/Overlay.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICaptureBaseData, 3 | IOverlayData, 4 | } from '../../interfaces/overlay/OverlayData.interface'; 5 | 6 | export class CaptureBase { 7 | #flagged_attributes: string[]; 8 | 9 | type: string; 10 | classification: string; 11 | attributes: { 12 | [key: string]: string; 13 | }; 14 | digest: string; 15 | 16 | constructor(captureBase: ICaptureBaseData) { 17 | this.type = captureBase.type; 18 | this.classification = captureBase.classification; 19 | this.attributes = captureBase.attributes; 20 | this.#flagged_attributes = captureBase.flagged_attributes; 21 | this.digest = captureBase.digest ?? ''; 22 | } 23 | 24 | get flaggedAttributes(): string[] { 25 | return this.#flagged_attributes; 26 | } 27 | } 28 | 29 | export class Overlay { 30 | #capture_base: string; 31 | 32 | type: string; 33 | digest: string; 34 | 35 | constructor(overlay: IOverlayData) { 36 | this.type = overlay.type; 37 | this.#capture_base = overlay.capture_base; 38 | this.digest = overlay.digest ?? ''; 39 | } 40 | 41 | get captureBase(): string { 42 | return this.#capture_base; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/overlayLibrary/types/overlay/OverlayType.ts: -------------------------------------------------------------------------------- 1 | import { BrandingOverlay } from './BrandingOverlay'; 2 | import { Overlay } from './Overlay'; 3 | import { 4 | CharacterEncodingOverlay, 5 | FormatOverlay, 6 | InformationOverlay, 7 | LabelOverlay, 8 | MetaOverlay, 9 | StandardOverlay, 10 | } from './SemanticOverlay'; 11 | 12 | export const OverlayType: { 13 | [key: string]: typeof Overlay | typeof BrandingOverlay; 14 | } = { 15 | 'spec/overlays/character_encoding/1.0': CharacterEncodingOverlay, 16 | 'spec/overlays/label/1.0': LabelOverlay, 17 | 'spec/overlays/information/1.0': InformationOverlay, 18 | 'spec/overlays/format/1.0': FormatOverlay, 19 | 'spec/overlays/standard/1.0': StandardOverlay, 20 | 'spec/overlays/meta/1.0': MetaOverlay, 21 | 'aries/overlays/branding/1.0': BrandingOverlay, 22 | }; 23 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/overlayLibrary/utils/localeDefaults.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Determine a fallback or default locale to use based on the overlay object 3 | * @param { object } overlayObject the object of translations from the overlay 4 | * @param { string } locale the selected language locale 5 | * @returns { string } a locale string 6 | */ 7 | export const localeDefault = (overlayObject: object, locale: string) => { 8 | const defaultLocale = 'en-CA'; 9 | try { 10 | if (locale in overlayObject) { 11 | // If the object has the locale selected by the switcher, go with that 12 | return locale; 13 | } 14 | // For BC Gov oca repository, default to the CA locale for en and fr 15 | // This will not be used when a general OCA solution is implemented 16 | if (['en', 'fr'].includes(locale)) { 17 | return `${locale}-CA`; 18 | } 19 | } catch (error) { 20 | console.error(error); 21 | } 22 | 23 | // en-CA as the default if nothing else found 24 | return defaultLocale; 25 | }; 26 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/plugins/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n'; 2 | import messages from '@intlify/unplugin-vue-i18n/messages'; 3 | 4 | export default createI18n({ 5 | locale: 'en', 6 | fallbackLocale: 'en', 7 | messages, 8 | }); 9 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/plugins/toasts/vueToastification.ts: -------------------------------------------------------------------------------- 1 | import { PluginOptions, POSITION, TYPE } from 'vue-toastification'; 2 | 3 | const toastOptions: PluginOptions = { 4 | position: POSITION.BOTTOM_RIGHT, 5 | 6 | toastDefaults: { 7 | // ToastOptions object for each type of toast 8 | [TYPE.ERROR]: { 9 | icon: 'pi pi-times-circle', 10 | }, 11 | [TYPE.SUCCESS]: { 12 | icon: 'pi pi-check-circle', 13 | }, 14 | [TYPE.INFO]: { 15 | icon: 'pi pi-info-circle', 16 | }, 17 | [TYPE.WARNING]: { 18 | icon: 'pi pi-exclamation-triangle', 19 | }, 20 | }, 21 | }; 22 | 23 | export default toastOptions; 24 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/store/commonStore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Place for storing common state that is not persisted in the backend. 3 | */ 4 | import { defineStore } from 'pinia'; 5 | import { computed, ref, Ref } from 'vue'; 6 | 7 | export const useCommonStore = defineStore('commonState', () => { 8 | // state 9 | const cardExpanded = ref(false); 10 | const sidebarOpen: Ref = ref(null); 11 | 12 | // getters 13 | const sidebarOpenClass = computed(() => { 14 | if (sidebarOpen.value === null) { 15 | // Use media queries 16 | return null; 17 | } else if (sidebarOpen.value) { 18 | // Default width 19 | return 'open'; 20 | } else { 21 | return 'closed'; // Mobile width 22 | } 23 | }); 24 | 25 | return { cardExpanded, sidebarOpenClass, sidebarOpen }; 26 | }); 27 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/store/index.ts: -------------------------------------------------------------------------------- 1 | export { useConfigStore } from './configStore'; 2 | 3 | // Tenant 4 | export { useConnectionStore } from './connectionStore'; 5 | export { useGovernanceStore } from './governanceStore'; 6 | export { useHolderStore } from './holderStore'; 7 | export { useIssuerStore } from './issuerStore'; 8 | export { useKeyManagementStore } from './keyManagementStore'; 9 | export { useTenantStore } from './tenantStore'; 10 | export { useTokenStore } from './tokenStore'; 11 | export { useVerifierStore } from './verifierStore'; 12 | export { useMessageStore } from './messageStore'; 13 | export { useReservationStore } from './reservationStore'; 14 | 15 | // Innkeeper 16 | export { useInnkeeperTokenStore } from './innkeeper/innkeeperTokenStore'; 17 | export { useInnkeeperTenantsStore } from './innkeeper/innkeeperTenantsStore'; 18 | export { useInnkeeperOidcStore } from './innkeeper/innkeeperOidcStore'; 19 | 20 | // OIDC 21 | export { useOidcStore } from './oidc/oidcStore'; 22 | 23 | // Log 24 | export { useLogStore } from './logStore'; 25 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/store/utils/fetchItem.ts: -------------------------------------------------------------------------------- 1 | import { useAcapyApi } from '../acapyApi'; 2 | import { AxiosRequestConfig } from 'axios'; 3 | import { Ref } from 'vue'; 4 | 5 | export async function fetchItem( 6 | url: string, 7 | id: string | undefined, 8 | error: Ref, 9 | loading: Ref, 10 | params: object = {} 11 | ): Promise { 12 | const acapyApi = useAcapyApi(); 13 | let dataUrl = url; 14 | if (id) { 15 | // Normalize if the caller supplies a trailing slash or not 16 | dataUrl = `${dataUrl.replace(/\/$/, '')}/${id}`; 17 | } 18 | console.log(` > fetchItem(${dataUrl})`); 19 | error.value = null; 20 | let result = null; 21 | 22 | await acapyApi 23 | .getHttp(dataUrl, params) 24 | .then((res: AxiosRequestConfig): void => { 25 | if (res?.data?.result) { 26 | // Some acapy resource item calls put things under "result" 27 | result = res.data.result; 28 | } else { 29 | result = res.data; 30 | } 31 | console.log(result); 32 | }) 33 | .catch((err: string): void => { 34 | error.value = err; 35 | }) 36 | .finally((): void => { 37 | loading.value = false; 38 | }); 39 | console.log(`< fetchItem(${dataUrl})`); 40 | if (error.value != null) { 41 | // throw error so $onAction.onError listeners can add their own handler 42 | throw error.value; 43 | } 44 | // return data so $onAction.after listeners can add their own handler 45 | return result; 46 | } 47 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 38 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/ApiKeys.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/InnkeeperUi.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | 19 | 25 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/TenantUi.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/connections/MyConnections.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/connections/MyInvitations.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/holder/MyHeldCredentials.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/innkeeper/InnkeeperApiKeys.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/innkeeper/InnkeeperReservations.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/innkeeper/InnkeeperReservationsHistory.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/innkeeper/InnkeeperServerConfig.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/innkeeper/InnkeeperTenants.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/issuance/CredentialDefinitions.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/issuance/MyIssuedCredentials.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/issuance/Schemas.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/messages/MyMessages.vue: -------------------------------------------------------------------------------- 1 | 4 | 7 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/oca/Oca.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/tenant/Developer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/tenant/Settings.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/views/verification/MyPresentations.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue'; 3 | const component: DefineComponent< 4 | Record, 5 | Record, 6 | any 7 | >; 8 | export default component; 9 | } 10 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/App.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import PrimeVue from 'primevue/config'; 3 | import { describe, expect, test } from 'vitest'; 4 | 5 | import { useTenantStore, useTokenStore } from '@/store'; 6 | import App from '@/App.vue'; 7 | 8 | const mountApp = () => 9 | mount(App, { 10 | global: { 11 | plugins: [PrimeVue], 12 | stubs: ['router-view'], 13 | }, 14 | }); 15 | 16 | describe('App', () => { 17 | test('document title is set', async () => { 18 | mountApp(); 19 | 20 | expect(document.title).toEqual('Tenant UI'); 21 | }); 22 | 23 | test('when app opens without refresh should call clear tenant local storage functions', async () => { 24 | mountApp(); 25 | 26 | expect(useTenantStore().clearTenant).toHaveBeenCalled(); 27 | expect(useTokenStore().clearToken).toHaveBeenCalled(); 28 | }); 29 | 30 | test.todo( 31 | 'Should test refresh does not call clear local storage functions (not sure if possible with session storage)' 32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/api/responses/acapy.ts: -------------------------------------------------------------------------------- 1 | const basic = { 2 | testResult: 'success', 3 | }; 4 | 5 | export default { basic }; 6 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/api/responses/index.ts: -------------------------------------------------------------------------------- 1 | export { default as acapyResponse } from './acapy'; 2 | export { default as connectionResponse } from './connection'; 3 | export { default as configResponse } from './config'; 4 | export { default as governanceResponse } from './governance'; 5 | export { default as holderResponse } from './holder'; 6 | export { default as messageResponse } from './message'; 7 | export { default as issuerResponse } from './issuer'; 8 | export { default as reservationResponse } from './reservation'; 9 | export { default as tenantResponse } from './tenant'; 10 | export { default as tokenResponse } from './token'; 11 | export { default as verifierResponse } from './verifier'; 12 | 13 | // Innkeeper 14 | export { default as innkeeperTokenResponse } from './innkeeper/token'; 15 | export { default as innkeeperTenantResponse } from './innkeeper/tenant'; 16 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/api/responses/innkeeper/token.ts: -------------------------------------------------------------------------------- 1 | const login = { 2 | token: 3 | 'eyJhbGciOiJFZERTQSJ9.eyJhIjogIjAifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk', 4 | }; 5 | 6 | export default { 7 | login, 8 | }; 9 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/api/responses/message.ts: -------------------------------------------------------------------------------- 1 | const messages = { 2 | results: [ 3 | { 4 | content: 'Test 2', 5 | state: 'sent', 6 | connection_id: 'bb6f8738-b3ee-46e4-b979-a84e6b269a0a', 7 | message_id: '8b28da74-ef77-4f64-83f0-d71084d6d50e', 8 | sent_time: '2023-06-26T22:34:14.055802Z', 9 | updated_at: '2023-06-26T22:34:14.058181Z', 10 | created_at: '2023-06-26T22:34:14.058181Z', 11 | }, 12 | { 13 | content: 'Test 1', 14 | state: 'sent', 15 | connection_id: 'bb6f8738-b3ee-46e4-b979-a84e6b269a0a', 16 | message_id: 'f5db3bd6-d085-452a-81ab-939e630224d2', 17 | sent_time: '2023-06-26T22:34:01.095062Z', 18 | updated_at: '2023-06-26T22:34:01.096728Z', 19 | created_at: '2023-06-26T22:34:01.096728Z', 20 | }, 21 | ], 22 | }; 23 | 24 | export default { 25 | messages, 26 | }; 27 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/api/responses/reservation.ts: -------------------------------------------------------------------------------- 1 | const makeReservationAutoApprove = { 2 | reservation_id: 'reservation_id', 3 | reservation_pwd: 'reservation_pwd', 4 | }; 5 | 6 | const makeReservationVerify = { 7 | reservation_id: 'reservation_id', 8 | reservation_pwd: '', 9 | }; 10 | 11 | const checkIn = { 12 | wallet_id: 'wallet_id', 13 | wallet_key: 'wallet_key', 14 | token: 'token', 15 | }; 16 | 17 | const reservation = { 18 | connect_to_endorser: { 19 | endorser_alias: ' ... ', 20 | ledger_id: ' ... ', 21 | }, 22 | contact_email: 'string', 23 | create_public_did: ['string'], 24 | created_at: '2021-12-31T23:59:59Z', 25 | reservation_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', 26 | state: 'requested', 27 | state_notes: 'string', 28 | tenant_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', 29 | tenant_name: 'line of business short name', 30 | updated_at: '2021-12-31T23:59:59Z', 31 | wallet_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', 32 | }; 33 | 34 | export default { 35 | checkIn, 36 | makeReservationAutoApprove, 37 | makeReservationVerify, 38 | reservation, 39 | }; 40 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/api/responses/token.ts: -------------------------------------------------------------------------------- 1 | const token = { 2 | token: 3 | 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ3YWxsZXRfaWQiOiJlYzhmZDBiMi1kNGM3LTQ1NzktYTIxMy0xNGI3NTNlZjNiNTMiLCJpYXQiOjE2ODc1NTkxMjMsImV4cCI6MTY4NzY0NTUyM30.7cGOtglbUEYRsYKi3Gpl77tsuVbn_swphJ62yWd1dkg', 4 | }; 5 | 6 | export default { 7 | token, 8 | }; 9 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/api/routes/acapy.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | 3 | import { API_PATH } from '@/helpers/constants'; 4 | import { acapyResponse } from '../responses'; 5 | 6 | export const successHandlers = [ 7 | http.all(API_PATH.TEST_TENANT_PROXY, () => 8 | HttpResponse.json(acapyResponse.basic) 9 | ), 10 | ]; 11 | 12 | export const authErrorHandlers = [ 13 | http.all(API_PATH.TEST_TENANT_PROXY, ({ request }) => 14 | HttpResponse.json( 15 | { 16 | reason: `Test: ${request.headers.get('Authorization')}`, 17 | }, 18 | { 19 | status: 401, 20 | } 21 | ) 22 | ), 23 | ]; 24 | 25 | export const unknownErrorHandlers = [ 26 | http.all(API_PATH.TEST_TENANT_PROXY, () => 27 | HttpResponse.json({}, { status: 500 }) 28 | ), 29 | ]; 30 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/api/routes/config.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | 3 | import { API_PATH } from '@/helpers/constants'; 4 | import { configResponse } from '../responses'; 5 | import { fullPathWithProxyTenant } from './utils/utils'; 6 | 7 | export const successHandlers = [ 8 | http.get(API_PATH.CONFIG, () => HttpResponse.json(configResponse.config)), 9 | http.get(fullPathWithProxyTenant(API_PATH.SERVER_PLUGINS), () => 10 | HttpResponse.json(configResponse.plugins) 11 | ), 12 | ]; 13 | 14 | export const unknownErrorHandlers = [ 15 | http.get(API_PATH.CONFIG, () => HttpResponse.json({}, { status: 500 })), 16 | ]; 17 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/api/routes/innkeeper/token.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | 3 | import { API_PATH } from '@/helpers/constants'; 4 | import { innkeeperTokenResponse } from '../../responses'; 5 | import { fullPathWithProxyTenant } from '../utils/utils'; 6 | 7 | export const successHandlers = [ 8 | http.post( 9 | fullPathWithProxyTenant(API_PATH.MULTITENANCY_TENANT_TOKEN('admin')), 10 | () => HttpResponse.json(innkeeperTokenResponse.login) 11 | ), 12 | ]; 13 | 14 | export const unknownErrorHandlers = [ 15 | http.post( 16 | fullPathWithProxyTenant(API_PATH.MULTITENANCY_TENANT_TOKEN('admin')), 17 | () => HttpResponse.json({}, { status: 500 }) 18 | ), 19 | ]; 20 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/api/routes/message.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | 3 | import { API_PATH } from '@/helpers/constants'; 4 | import { messageResponse } from '../responses'; 5 | import { fullPathWithProxyTenant } from './utils/utils'; 6 | 7 | export const successHandlers = [ 8 | http.post( 9 | fullPathWithProxyTenant(API_PATH.BASICMESSAGES_SEND('connId')), 10 | () => HttpResponse.json({ test: 'test' }) 11 | ), 12 | http.get(fullPathWithProxyTenant(API_PATH.BASICMESSAGES), () => 13 | HttpResponse.json(messageResponse.messages) 14 | ), 15 | ]; 16 | 17 | export const unknownErrorHandlers = [ 18 | http.post( 19 | fullPathWithProxyTenant(API_PATH.BASICMESSAGES_SEND('connId')), 20 | () => HttpResponse.json({}, { status: 500 }) 21 | ), 22 | ]; 23 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/api/routes/token.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | 3 | import { API_PATH } from '@/helpers/constants'; 4 | import { tokenResponse } from '../responses'; 5 | 6 | export const successHandlers = [ 7 | http.post(API_PATH.MULTITENANCY_WALLET_TOKEN('username'), () => 8 | HttpResponse.json(tokenResponse.token) 9 | ), 10 | ]; 11 | 12 | export const unknownErrorHandlers = [ 13 | http.post(API_PATH.MULTITENANCY_WALLET_TOKEN('username'), () => 14 | HttpResponse.json({}, { status: 500 }) 15 | ), 16 | ]; 17 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/api/routes/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { API_PATH } from '@/helpers/constants'; 2 | 3 | export function fullPathWithProxyTenant(prefix: string) { 4 | return API_PATH.TEST_TENANT_PROXY + prefix; 5 | } 6 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/api/routes/verifier.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | import { API_PATH } from '@/helpers/constants'; 3 | import { verifierResponse } from '../responses'; 4 | import { fullPathWithProxyTenant } from './utils/utils'; 5 | 6 | export const successHandlers = [ 7 | http.get(fullPathWithProxyTenant(API_PATH.PRESENT_PROOF_20_RECORDS), () => 8 | HttpResponse.json(verifierResponse.presentProofRecords) 9 | ), 10 | http.post( 11 | fullPathWithProxyTenant(API_PATH.PRESENT_PROOF_20_SEND_REQUEST), 12 | () => HttpResponse.json(verifierResponse.presentProofSendRequest) 13 | ), 14 | http.delete( 15 | fullPathWithProxyTenant(API_PATH.PRESENT_PROOF_20_RECORD('test-uuid')), 16 | () => HttpResponse.json({}) 17 | ), 18 | ]; 19 | 20 | export const unknownErrorHandlers = [ 21 | http.get(fullPathWithProxyTenant(API_PATH.PRESENT_PROOF_20_RECORDS), () => 22 | HttpResponse.json({}, { status: 500 }) 23 | ), 24 | http.post( 25 | fullPathWithProxyTenant(API_PATH.PRESENT_PROOF_20_SEND_REQUEST), 26 | () => HttpResponse.json({}, { status: 500 }) 27 | ), 28 | http.delete( 29 | fullPathWithProxyTenant(API_PATH.PRESENT_PROOF_20_RECORD('test-uuid')), 30 | () => HttpResponse.json({}, { status: 500 }) 31 | ), 32 | ]; 33 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/store/common.ts: -------------------------------------------------------------------------------- 1 | const store: { [key: string]: any } = { 2 | sidebarOpen: { 3 | value: false, 4 | }, 5 | }; 6 | 7 | export { store }; 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/store/index.ts: -------------------------------------------------------------------------------- 1 | export { store as commonStore } from './common'; 2 | export { store as configStore } from './config'; 3 | export { store as connectionStore } from './connection'; 4 | export { store as holderStore } from './holder'; 5 | export { store as issuerStore } from './issuer'; 6 | export { store as governanceStore } from './governance'; 7 | export { store as messageStore } from './message'; 8 | export { store as reservationStore } from './reservation'; 9 | export { store as tenantStore } from './tenant'; 10 | export { store as tokenStore } from './token'; 11 | export { store as verifierStore } from './verifier'; 12 | 13 | // Innkeeper 14 | export { store as innkeeperTenantsStore } from './innkeeper/tenants'; 15 | export { store as innkeeperTokenStore } from './innkeeper/token'; 16 | export { store as innkeeperOidcStore } from './innkeeper/oidc'; 17 | 18 | // Oidc 19 | export { store as oidcStore } from './oidc/oidc'; 20 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/store/innkeeper/oidc.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | const store: { [key: string]: any } = { 4 | login: vi.fn().mockResolvedValue('success'), 5 | }; 6 | 7 | export { store }; 8 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/store/innkeeper/tenants.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | const tenant = { 4 | created_public_did: ['bcovrin-test'], 5 | wallet_id: 'ec8fd0b2-d4c7-4579-a213-14b753ef3b53', 6 | connect_to_endorser: [ 7 | { 8 | endorser_alias: 'endorser', 9 | ledger_id: 'bcovrin-test', 10 | }, 11 | ], 12 | tenant_name: 'Tenant', 13 | created_at: '2023-06-23T22:24:38.228607Z', 14 | tenant_id: '90a3d1fb-c011-4e23-a0b6-fc37d2368467', 15 | state: 'active', 16 | enable_ledger_switch: true, 17 | }; 18 | 19 | const store: { [key: string]: any } = { 20 | tenants: { 21 | value: [tenant], 22 | }, 23 | serverConfig: { 24 | value: { 25 | config: { 26 | version: '1.2.2', 27 | }, 28 | }, 29 | }, 30 | listTenants: vi.fn().mockResolvedValue([tenant]).mockRejectedValue('fail'), 31 | getServerConfig: vi.fn().mockResolvedValue('success'), 32 | }; 33 | 34 | export { store }; 35 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/store/innkeeper/token.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | const store: { [key: string]: any } = { 4 | clearToken: vi.fn(), 5 | innkeeperReady: vi.fn().mockResolvedValue(false), 6 | login: vi.fn().mockResolvedValue('success'), 7 | }; 8 | 9 | export { store }; 10 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/store/issuer.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | // Some properties have been removed 4 | const credentials = [ 5 | { 6 | cred_ex_record: { 7 | connection_id: '973a7f8d-7c37-4539-a6a0-83ea8b589103', 8 | state: 'done', 9 | created_at: '2023-07-13T23:06:50.592455Z', 10 | cred_ex_id: '317850b8-ac52-4dd5-9644-3a898425ecbc', 11 | credential_definition_id: 'test-cred_def_id', 12 | auto_offer: false, 13 | auto_issue: true, 14 | trace: false, 15 | cred_offer: { 16 | schema_id: 'KU5ZTAFVKtNKFNpD5Aypzb:2:Test Schema:1.0', 17 | cred_def_id: 'test-cred_def_id', 18 | }, 19 | }, 20 | indy: { 21 | cred_ex_id: '317850b8-ac52-4dd5-9644-3a898425ecbc', 22 | created_at: '2023-07-13T23:06:50.592455Z', 23 | rev_reg_id: 24 | 'WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0', 25 | cred_rev_id: '1', 26 | }, 27 | }, 28 | ]; 29 | 30 | const store: { [key: string]: any } = { 31 | credentials: { 32 | value: credentials, 33 | }, 34 | listCredentials: vi.fn().mockResolvedValue(credentials), 35 | selectedCredential: null, 36 | offerCredential: vi.fn().mockResolvedValue(true), 37 | revokeCredential: vi.fn().mockResolvedValue(true), 38 | deleteCredentialExchange: vi.fn().mockResolvedValue(true), 39 | getCredentials: vi.fn().mockResolvedValue({}), 40 | }; 41 | 42 | export { store }; 43 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/store/message.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | const store: { [key: string]: any } = { 4 | messages: { 5 | value: [ 6 | { 7 | connection_id: 'test-connection-id', 8 | content: 'test-content', 9 | created_at: '2023-07-04T17:16:13.771616Z', 10 | displayTime: true, 11 | message_id: '7f0ba8a4-c798-402d-895a-d68c777ea294', 12 | sent_time: '2023-07-04T17:16:13.769151Z', 13 | state: 'sent', 14 | updated_at: '2023-07-04T17:16:13.771616Z', 15 | test: 'test', 16 | }, 17 | ], 18 | }, 19 | listMessages: vi.fn().mockResolvedValue([ 20 | { 21 | connection_id: 'test-connection-id', 22 | content: 'test-content', 23 | created_at: '2023-07-04T17:16:13.771616Z', 24 | displayTime: true, 25 | message_id: '7f0ba8a4-c798-402d-895a-d68c777ea294', 26 | sent_time: '2023-07-04T17:16:13.769151Z', 27 | state: 'sent', 28 | updated_at: '2023-07-04T17:16:13.771616Z', 29 | }, 30 | ]), 31 | newMessage: '', 32 | selectedMessage: null, 33 | sendMessage: vi.fn().mockResolvedValue(true), 34 | }; 35 | 36 | export { store }; 37 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/store/oidc/oidc.ts: -------------------------------------------------------------------------------- 1 | const store: { [key: string]: any } = { 2 | user: null, 3 | }; 4 | 5 | export { store }; 6 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/store/reservation.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | const store: { [key: string]: any } = { 4 | resetState: vi.fn(), 5 | status: { 6 | value: '', 7 | }, 8 | }; 9 | 10 | export { store }; 11 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/store/tenant.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | const store: { [key: string]: any } = { 4 | clearTenant: vi.fn(), 5 | tenant: { 6 | value: { 7 | tenant_name: 'test', 8 | }, 9 | }, 10 | endorserInfo: { 11 | value: null, 12 | }, 13 | serverConfig: { 14 | value: { 15 | config: { 16 | version: '1.2.2', 17 | }, 18 | }, 19 | }, 20 | getEndorserInfo: vi.fn().mockResolvedValue({ 21 | endorser_did: 'SVfHGCEEvEFmpBPcxgNqRR', 22 | endorser_name: 'endorser', 23 | }), 24 | getIssuanceStatus: vi.fn().mockResolvedValue('success'), 25 | getSelf: vi.fn().mockResolvedValue('success'), 26 | getTenantConfig: vi.fn().mockResolvedValue('success'), 27 | getServerConfig: vi.fn().mockResolvedValue('success'), 28 | }; 29 | 30 | export { store }; 31 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/__mocks__/store/token.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | const store: { [key: string]: any } = { 4 | clearToken: vi.fn(), 5 | login: vi.fn().mockResolvedValue({}), 6 | token: { 7 | value: 'token', 8 | }, 9 | }; 10 | 11 | export { store }; 12 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/commonTests.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'vitest'; 2 | 3 | /** 4 | * @param store - the store to test 5 | * @param func - the function to call 6 | * @param loadingKey - the key to check for loading 7 | */ 8 | const testSuccessResponse = async ( 9 | store: any, 10 | func: () => void, 11 | loadingKey: string | undefined 12 | ) => { 13 | if (loadingKey) { 14 | expect(store[loadingKey]).toEqual(true); 15 | } 16 | const response = await func; 17 | 18 | expect(response).not.toBeNull(); 19 | if (loadingKey) { 20 | expect(store[loadingKey]).toEqual(false); 21 | } 22 | expect(store.error).toBeNull(); 23 | }; 24 | 25 | /** 26 | * @param store - the store to test 27 | * @param func - the function to call 28 | * @param loadingKey - the key to check for loading 29 | */ 30 | const testErrorResponse = async ( 31 | store: any, 32 | func: () => void, 33 | loadingKey: string 34 | ) => { 35 | expect(store[loadingKey]).toEqual(true); 36 | await expect(func).rejects.toThrow(); 37 | expect(store[loadingKey]).toEqual(false); 38 | expect(store.error).not.toBeNull(); 39 | }; 40 | 41 | export { testErrorResponse, testSuccessResponse }; 42 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/about/Acapy.test.ts: -------------------------------------------------------------------------------- 1 | import { useTenantStore } from '@/store'; 2 | import { createTestingPinia } from '@pinia/testing'; 3 | import { mount } from '@vue/test-utils'; 4 | import PrimeVue from 'primevue/config'; 5 | import { describe, expect, test, vi } from 'vitest'; 6 | 7 | import Acapy from '@/components/about/Acapy.vue'; 8 | 9 | // Mocks 10 | vi.mock('vue-router'); 11 | 12 | const mockRouter = async (name = 'test') => { 13 | const routerMock = await import('vue-router'); 14 | routerMock.useRoute = vi.fn().mockReturnValue({ 15 | name, 16 | }); 17 | routerMock.useRouter = vi.fn().mockReturnValue({ 18 | push: vi.fn(), 19 | }); 20 | }; 21 | 22 | const createWrapper = () => 23 | mount(Acapy, { 24 | global: { 25 | plugins: [PrimeVue, createTestingPinia()], 26 | }, 27 | }); 28 | 29 | describe('Acapy', async () => { 30 | test('mount matches snapshot with expected values', async () => { 31 | await mockRouter(); 32 | const tenantStore = useTenantStore(); 33 | expect(createWrapper().html()).toMatchSnapshot(); 34 | expect(tenantStore.getServerConfig).toHaveBeenCalled(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/about/Business.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestingPinia } from '@pinia/testing'; 2 | import { mount } from '@vue/test-utils'; 3 | import PrimeVue from 'primevue/config'; 4 | import { describe, expect, test } from 'vitest'; 5 | 6 | import Business from '@/components/about/Business.vue'; 7 | 8 | describe('Business', async () => { 9 | test('mount matches snapshot with expected values', () => { 10 | const wrapper = mount(Business, { 11 | global: { 12 | plugins: [PrimeVue, createTestingPinia()], 13 | }, 14 | }); 15 | 16 | expect(wrapper.html()).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/about/Traction.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestingPinia } from '@pinia/testing'; 2 | import { mount } from '@vue/test-utils'; 3 | import PrimeVue from 'primevue/config'; 4 | import { describe, expect, test } from 'vitest'; 5 | 6 | import Traction from '@/components/about/Traction.vue'; 7 | 8 | describe('Traction', async () => { 9 | test('mount matches snapshot with expected values', () => { 10 | const wrapper = mount(Traction, { 11 | global: { 12 | plugins: [PrimeVue, createTestingPinia()], 13 | }, 14 | }); 15 | 16 | expect(wrapper.html()).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/about/__snapshots__/Business.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Business > mount matches snapshot with expected values 1`] = ` 4 | "
5 |
About Business
6 | 7 |
8 |
" 9 | `; 10 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/common/LocaleSwitcher.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import PrimeVue from 'primevue/config'; 3 | import { describe, expect, test } from 'vitest'; 4 | 5 | import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue'; 6 | 7 | const mountLocaleSwitcher = () => 8 | mount(LocaleSwitcher, { 9 | global: { 10 | plugins: [PrimeVue], 11 | }, 12 | }); 13 | 14 | describe('LocaleSwitcher', () => { 15 | test('mount matches snapshot with expected values', async () => { 16 | const wrapper = mountLocaleSwitcher(); 17 | 18 | expect(wrapper.html()).toMatchSnapshot(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/common/QRCode.test.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import PrimeVue from 'primevue/config'; 3 | import { describe, expect, test, vi } from 'vitest'; 4 | import VueToastificationPlugin from 'vue-toastification'; 5 | 6 | import QRCode from '@/components/common/QRCode.vue'; 7 | 8 | Object.assign(navigator, { 9 | clipboard: { 10 | writeText: vi.fn().mockImplementation(() => Promise.resolve()), 11 | }, 12 | }); 13 | 14 | const writeTextSpy = vi.spyOn(navigator.clipboard, 'writeText'); 15 | 16 | const mountQRCode = () => 17 | shallowMount(QRCode, { 18 | props: { 19 | qrContent: 'test', 20 | }, 21 | global: { 22 | plugins: [PrimeVue, VueToastificationPlugin], 23 | }, 24 | }); 25 | 26 | describe('QRCode', () => { 27 | test('toast and copy_to_clipboard called when button clicked', async () => { 28 | const wrapper = mountQRCode(); 29 | const wrapperVm = wrapper.vm as unknown as typeof QRCode; 30 | const toastSpy = vi.spyOn(wrapperVm.toast, 'info'); 31 | 32 | wrapper.getComponent({ name: 'Button' }).trigger('click'); 33 | 34 | expect(toastSpy).toHaveBeenCalled(); 35 | expect(writeTextSpy).toHaveBeenCalled(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/common/SessionTimeoutModal.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestingPinia } from '@pinia/testing'; 2 | import { mount } from '@vue/test-utils'; 3 | import PrimeVue from 'primevue/config'; 4 | import { describe, expect, test } from 'vitest'; 5 | 6 | import SessionTimeoutModal from '@/components/common/SessionTimeoutModal.vue'; 7 | 8 | const mountSessionTimeoutModal = () => 9 | mount(SessionTimeoutModal, { 10 | global: { 11 | plugins: [PrimeVue, createTestingPinia()], 12 | stubs: ['Dialog'], 13 | }, 14 | }); 15 | 16 | describe('SessionTimeoutModal', () => { 17 | test('modal is hippen on initial mount', async () => { 18 | const wrapper = mountSessionTimeoutModal(); 19 | 20 | expect(wrapper.getComponent({ name: 'Dialog' }).attributes().visible).toBe( 21 | 'false' 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/common/SessionTimer.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestingPinia } from '@pinia/testing'; 2 | import { mount } from '@vue/test-utils'; 3 | import PrimeVue from 'primevue/config'; 4 | import { describe, expect, test, vi } from 'vitest'; 5 | 6 | import SessionTimer from '@/components/common/SessionTimer.vue'; 7 | 8 | const mountSessionTimer = () => 9 | mount(SessionTimer, { 10 | global: { 11 | plugins: [PrimeVue, createTestingPinia()], 12 | stubs: ['Dialog', 'router-location'], 13 | }, 14 | }); 15 | 16 | describe('SessionTimer', () => { 17 | test('mounting creates the event listeners', async () => { 18 | const spy = vi.spyOn(window, 'addEventListener'); 19 | mountSessionTimer(); 20 | 21 | expect(spy).toHaveBeenCalledTimes(4); 22 | }); 23 | 24 | test('unmounting clears the intervals', async () => { 25 | const spy = vi.spyOn(globalThis, 'clearInterval'); 26 | const wrapper = mountSessionTimer(); 27 | await wrapper.unmount(); 28 | 29 | expect(spy).toHaveBeenCalledTimes(2); 30 | }); 31 | 32 | test('modal is hidden when countdown is false', async () => { 33 | const wrapper = mountSessionTimer(); 34 | 35 | expect(wrapper.getComponent({ name: 'Dialog' }).attributes().visible).toBe( 36 | 'false' 37 | ); 38 | }); 39 | 40 | test.todo('test the interval functionality'); 41 | }); 42 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/common/SkeletonCard.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import PrimeVue from 'primevue/config'; 3 | import { describe, expect, test } from 'vitest'; 4 | 5 | import SkeletonCard from '@/components/common/SkeletonCard.vue'; 6 | 7 | describe('SkeletonCard', () => { 8 | test('mount matches snapshot with expected values', async () => { 9 | const wrapper = mount(SkeletonCard, { 10 | global: { 11 | plugins: [PrimeVue], 12 | }, 13 | }); 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/common/StatusChip.test.ts: -------------------------------------------------------------------------------- 1 | import { flushPromises, mount } from '@vue/test-utils'; 2 | import PrimeVue from 'primevue/config'; 3 | import { describe, expect, test } from 'vitest'; 4 | 5 | import StatusChip from '@/components/common/StatusChip.vue'; 6 | 7 | describe('StatusChip', () => { 8 | test('mount matches snapshot with expected values', async () => { 9 | const wrapper = mount(StatusChip, { 10 | props: { 11 | status: 'kebabTest', 12 | class: 'test-class', 13 | }, 14 | global: { 15 | plugins: [PrimeVue], 16 | }, 17 | }); 18 | 19 | await flushPromises(); 20 | 21 | const expectedClasses = [ 22 | 'test-class', 23 | 'kebab-test', 24 | 'status-chip', 25 | 'p-chip', 26 | ]; 27 | 28 | // Tests that all expected classes are present in component 29 | expect(expectedClasses.every((c) => wrapper.classes().includes(c))).toBe( 30 | true 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/common/__snapshots__/SkeletonCard.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`SkeletonCard > mount matches snapshot with expected values 1`] = ` 4 | "
5 |
6 | 7 |
8 | 9 | 10 |
11 |
12 | 13 |
14 | 15 | 16 |
17 |
" 18 | `; 19 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/connections/Invitations.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestingPinia } from '@pinia/testing'; 2 | import { mount } from '@vue/test-utils'; 3 | import PrimeVue from 'primevue/config'; 4 | import ConfirmationService from 'primevue/confirmationservice'; 5 | import { describe, expect, test } from 'vitest'; 6 | 7 | import Invitations from '@/components/connections/Invitations.vue'; 8 | 9 | const mountInvitations = () => 10 | mount(Invitations, { 11 | global: { 12 | plugins: [PrimeVue, createTestingPinia(), ConfirmationService], 13 | }, 14 | }); 15 | 16 | describe('Invitations', () => { 17 | test('mount renders with expected components', async () => { 18 | const wrapper = mountInvitations(); 19 | 20 | wrapper.getComponent({ name: 'DataTable' }); 21 | expect( 22 | wrapper.findAllComponents({ name: 'CreateConnection' }) 23 | ).toHaveLength(1); 24 | }); 25 | 26 | test('table body is rendered with expected values', async () => { 27 | const wrapper = mountInvitations(); 28 | const expectedTexts = ['', 'test.alias', 'once', 'connections/1.0']; 29 | 30 | // td is an expected text or valid date 31 | wrapper.findAll('tbody td').forEach((td) => { 32 | const text = td.text(); 33 | expect(expectedTexts.includes(text) || !isNaN(Date.parse(text))).toBe( 34 | true 35 | ); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/connections/acceptInvitation/AcceptInvitation.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import PrimeVue from 'primevue/config'; 3 | import { describe, expect, test } from 'vitest'; 4 | 5 | import AcceptInvitation from '@/components/connections/acceptInvitation/AcceptInvitation.vue'; 6 | 7 | const mountAcceptInvitation = () => 8 | mount(AcceptInvitation, { 9 | global: { 10 | plugins: [PrimeVue], 11 | stubs: ['Dialog'], 12 | }, 13 | }); 14 | 15 | describe('AcceptInvitation', () => { 16 | test('mount has expected components with dialog not visible', async () => { 17 | const wrapper = mountAcceptInvitation(); 18 | 19 | wrapper.getComponent({ name: 'Button' }); 20 | expect(wrapper.getComponent({ name: 'Dialog' }).attributes().visible).toBe( 21 | 'false' 22 | ); 23 | }); 24 | 25 | test('dialog becomes visible on button click', async () => { 26 | const wrapper = mountAcceptInvitation(); 27 | 28 | await wrapper.getComponent({ name: 'Button' }).trigger('click'); 29 | expect(wrapper.getComponent({ name: 'Dialog' }).attributes().visible).toBe( 30 | 'true' 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/connections/createContact/CreateContact.test.ts: -------------------------------------------------------------------------------- 1 | import { flushPromises, mount } from '@vue/test-utils'; 2 | import PrimeVue from 'primevue/config'; 3 | import { describe, expect, test } from 'vitest'; 4 | 5 | import CreateConnection from '@/components/connections/createConnection/CreateConnection.vue'; 6 | 7 | const mountCreateConnection = () => 8 | mount(CreateConnection, { 9 | props: { 10 | multi: true, 11 | }, 12 | global: { 13 | plugins: [PrimeVue], 14 | stubs: ['Dialog'], 15 | }, 16 | }); 17 | 18 | describe('CreateConnection', async () => { 19 | test('renders as with expected ', async () => { 20 | const wrapper = mountCreateConnection(); 21 | 22 | wrapper.getComponent({ name: 'Button' }); 23 | 24 | expect(wrapper.getComponent({ name: 'Dialog' }).attributes().visible).toBe( 25 | 'false' 26 | ); 27 | }); 28 | 29 | test('button click opens modal', async () => { 30 | const wrapper = mountCreateConnection(); 31 | 32 | wrapper.getComponent({ name: 'Button' }).trigger('click'); 33 | await flushPromises(); 34 | expect(wrapper.getComponent({ name: 'Dialog' }).attributes().visible).toBe( 35 | 'true' 36 | ); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/connections/didExchange/DidExchange.test.ts: -------------------------------------------------------------------------------- 1 | import { flushPromises, mount } from '@vue/test-utils'; 2 | import PrimeVue from 'primevue/config'; 3 | import { describe, expect, test } from 'vitest'; 4 | 5 | import DidExchange from '@/components/connections/didExchange/DidExchange.vue'; 6 | 7 | const mountDidExchange = () => 8 | mount(DidExchange, { 9 | global: { 10 | plugins: [PrimeVue], 11 | stubs: ['Dialog'], 12 | }, 13 | }); 14 | 15 | describe('DidExchange', () => { 16 | test('mount has expected components with modal closed', async () => { 17 | const wrapper = mountDidExchange(); 18 | 19 | wrapper.getComponent({ name: 'Button' }); 20 | expect(wrapper.getComponent({ name: 'Dialog' }).attributes().visible).toBe( 21 | 'false' 22 | ); 23 | }); 24 | 25 | test('modal opens on button click', async () => { 26 | const wrapper = mountDidExchange(); 27 | 28 | wrapper.getComponent({ name: 'Button' }).trigger('click'); 29 | await flushPromises(); 30 | expect(wrapper.getComponent({ name: 'Dialog' }).attributes().visible).toBe( 31 | 'true' 32 | ); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/connections/editContact/DeleteContact.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestingPinia } from '@pinia/testing'; 2 | import { mount } from '@vue/test-utils'; 3 | import PrimeVue from 'primevue/config'; 4 | import ConfirmationService from 'primevue/confirmationservice'; 5 | import { describe, expect, test, vi } from 'vitest'; 6 | 7 | import DeleteConnection from '@/components/connections/editConnection/DeleteConnection.vue'; 8 | 9 | const mountDeleteConnection = () => 10 | mount(DeleteConnection, { 11 | props: { 12 | connectionId: 'test-connection-id', 13 | }, 14 | global: { 15 | plugins: [PrimeVue, createTestingPinia(), ConfirmationService], 16 | }, 17 | }); 18 | 19 | describe('DeleteConnection', () => { 20 | test('mount has expected components and calls delete on click', async () => { 21 | const wrapper = mountDeleteConnection(); 22 | const wrapperVm = wrapper.vm as unknown as typeof DeleteConnection; 23 | const spy = vi.spyOn(wrapperVm.confirm, 'require'); 24 | wrapper 25 | .getComponent({ name: 'Button', props: { icon: 'pi pi-trash' } }) 26 | .trigger('click'); 27 | expect(spy).toHaveBeenCalled(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/connections/editContact/EditContact.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import PrimeVue from 'primevue/config'; 3 | import { describe, expect, test } from 'vitest'; 4 | 5 | import EditConnection from '@/components/connections/editConnection/EditConnection.vue'; 6 | 7 | const mountEditConnection = () => 8 | mount(EditConnection, { 9 | props: { 10 | connectionId: 'test-connection-id', 11 | }, 12 | global: { 13 | plugins: [PrimeVue], 14 | stubs: ['Dialog'], 15 | }, 16 | }); 17 | 18 | describe('EditConnection', () => { 19 | test('mount matches expected components with modal closed', async () => { 20 | const wrapper = mountEditConnection(); 21 | 22 | wrapper.getComponent({ name: 'Button' }); 23 | expect(wrapper.getComponent({ name: 'Dialog' }).attributes().visible).toBe( 24 | 'false' 25 | ); 26 | }); 27 | 28 | test('modal becomes visible on button click', async () => { 29 | const wrapper = mountEditConnection(); 30 | 31 | await wrapper.getComponent({ name: 'Button' }).trigger('click'); 32 | expect(wrapper.getComponent({ name: 'Dialog' }).attributes().visible).toBe( 33 | 'true' 34 | ); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/connections/messageContact/MessageContactList.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestingPinia } from '@pinia/testing'; 2 | import { flushPromises, mount } from '@vue/test-utils'; 3 | import PrimeVue from 'primevue/config'; 4 | import { describe, expect, test } from 'vitest'; 5 | 6 | import MessageConnectionList from '@/components/connections/messageConnection/MessageConnectionList.vue'; 7 | import { useMessageStore } from '@/store'; 8 | 9 | const mountMessageConnection = () => 10 | mount(MessageConnectionList, { 11 | props: { 12 | connectionId: 'test-connection-id', 13 | connectionName: 'test-connection-name', 14 | }, 15 | global: { 16 | plugins: [PrimeVue, createTestingPinia()], 17 | }, 18 | }); 19 | 20 | describe('MessageConnection', () => { 21 | test('mount calls listMessage and list elements have expected classes', async () => { 22 | const wrapper = await mountMessageConnection(); 23 | const store = useMessageStore(); 24 | await flushPromises(); 25 | 26 | expect(store.listMessages).toHaveBeenCalled(); 27 | 28 | // This is somewhat brittle. Possibly better to use a data-testid. 29 | expect(wrapper.html()).toContain('class="mine message"'); 30 | expect(wrapper.html()).toContain('class="bubble"'); 31 | expect(wrapper.html()).toContain('class="time display"'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/innkeeper/InnkeeperLogin.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestingPinia } from '@pinia/testing'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import PrimeVue from 'primevue/config'; 4 | import { describe, expect, test } from 'vitest'; 5 | 6 | import InnkeeperLogin from '@/components/innkeeper/InnkeeperLogin.vue'; 7 | 8 | import { configStore } from '../../__mocks__/store'; 9 | 10 | const mountLogin = () => 11 | shallowMount(InnkeeperLogin, { 12 | global: { 13 | plugins: [PrimeVue, createTestingPinia()], 14 | }, 15 | }); 16 | 17 | describe('InnkeeperLogin', async () => { 18 | test('renders InnkeeperLoginForm when showInnkeeperAdminLogin true', async () => { 19 | const wrapper = mountLogin(); 20 | wrapper.getComponent({ name: 'InnkeeperLoginForm' }); 21 | }); 22 | 23 | test('does not render InnkeeperLoginForm when showInnkeeperAdminLogin false', async () => { 24 | configStore.config.frontend.showInnkeeperAdminLogin = false; 25 | const wrapper = mountLogin(); 26 | 27 | expect( 28 | wrapper.findComponent({ name: 'InnkeeperLoginForm' }).exists() 29 | ).toBeFalsy(); 30 | }); 31 | 32 | test('InnkeeperLoginOidc shows when oidc active ', async () => { 33 | configStore.config.frontend.oidc.active = true; 34 | const wrapper = mountLogin(); 35 | 36 | wrapper.getComponent({ name: 'InnkeeperLoginOidc' }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/innkeeper/tenants/Tenants.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestingPinia } from '@pinia/testing'; 2 | import { mount } from '@vue/test-utils'; 3 | import PrimeVue from 'primevue/config'; 4 | import { describe, expect, test } from 'vitest'; 5 | 6 | import Tenants from '@/components/innkeeper/tenants/Tenants.vue'; 7 | 8 | const mountTenants = () => 9 | mount(Tenants, { 10 | global: { 11 | plugins: [PrimeVue, createTestingPinia()], 12 | }, 13 | }); 14 | 15 | describe('InnkeeperLogin', async () => { 16 | test('formattedTenants formats and the data renders in table body', async () => { 17 | const wrapper = mountTenants(); 18 | const expectedTexts = ['', 'Tenant']; 19 | 20 | // td is an expected text or valid date 21 | wrapper.findAll('tbody td').forEach((td) => { 22 | const text = td.text(); 23 | expect(expectedTexts.includes(text) || !isNaN(Date.parse(text))).toBe( 24 | true 25 | ); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/issuance/credentialDefinitions/CredentialDefinitions.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestingPinia } from '@pinia/testing'; 2 | import { mount } from '@vue/test-utils'; 3 | import PrimeVue from 'primevue/config'; 4 | import ConfirmationService from 'primevue/confirmationservice'; 5 | import { describe, expect, test } from 'vitest'; 6 | 7 | import CredentialDefinitions from '@/components/issuance/credentialDefinitions/CredentialDefinitions.vue'; 8 | 9 | import { configStore } from '../../../__mocks__/store'; 10 | 11 | const mountCredentialDefinitions = () => 12 | mount(CredentialDefinitions, { 13 | global: { 14 | plugins: [PrimeVue, createTestingPinia(), ConfirmationService], 15 | }, 16 | }); 17 | 18 | describe('CredentialDefinitions', () => { 19 | test('mount has expected components', async () => { 20 | const wrapper = mountCredentialDefinitions(); 21 | 22 | wrapper.getComponent({ name: 'CreateCredentialDefinition' }); 23 | }); 24 | 25 | test('mount with showWritableComponents false does not have CreateSchema component', async () => { 26 | configStore.config.frontend.showWritableComponents = false; 27 | const wrapper = mountCredentialDefinitions(); 28 | 29 | expect( 30 | wrapper.findComponent({ name: 'CreateCredentialDefinition' }).exists() 31 | ).toBe(false); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/layout/Footer.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import { describe, expect, test } from 'vitest'; 3 | 4 | import Footer from '@/components/layout/Footer.vue'; 5 | 6 | describe('Header', async () => { 7 | test('mount matches snapshot with expected values', () => { 8 | const wrapper = mount(Footer); 9 | 10 | expect(wrapper.html()).toMatchSnapshot(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/layout/Header.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import PrimeVue from 'primevue/config'; 3 | import { describe, expect, test } from 'vitest'; 4 | import { createTestingPinia } from '@pinia/testing'; 5 | 6 | import Header from '@/components/layout/Header.vue'; 7 | 8 | describe('Header', async () => { 9 | test('mount matches snapshot with expected values', async () => { 10 | const wrapper = mount(Header, { 11 | global: { 12 | plugins: [PrimeVue, createTestingPinia()], 13 | stubs: [ 14 | 'router-link', 15 | 'ProfileButton', 16 | 'LocaleSwitcher', 17 | 'SessionTimer', 18 | ], 19 | }, 20 | }); 21 | 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/layout/Sidebar.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import PrimeVue from 'primevue/config'; 3 | import { describe, expect, test } from 'vitest'; 4 | 5 | import Sidebar from '@/components/layout/Sidebar.vue'; 6 | 7 | describe('Sidebar', async () => { 8 | test('mount matches snapshot with expected values', () => { 9 | const wrapper = mount(Sidebar, { 10 | global: { 11 | plugins: [PrimeVue], 12 | stubs: ['router-link'], 13 | }, 14 | }); 15 | 16 | expect(wrapper.html()).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/layout/__snapshots__/AppLayout.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`AppLayout > mount matches snapshot with expected values 1`] = `""`; 4 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/layout/__snapshots__/Footer.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Header > mount matches snapshot with expected values 1`] = ` 4 | "" 8 | `; 9 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/layout/__snapshots__/Header.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Header > mount matches snapshot with expected values 1`] = ` 4 | "" 15 | `; 16 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/layout/innkeeper/Header.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import PrimeVue from 'primevue/config'; 3 | import { describe, expect, test } from 'vitest'; 4 | import { createTestingPinia } from '@pinia/testing'; 5 | 6 | import Header from '@/components/layout/innkeeper/Header.vue'; 7 | 8 | describe('Header', async () => { 9 | test('mount matches snapshot with expected values', async () => { 10 | const wrapper = mount(Header, { 11 | global: { 12 | plugins: [PrimeVue, createTestingPinia()], 13 | stubs: [ 14 | 'router-link', 15 | 'ProfileButton', 16 | 'LocaleSwitcher', 17 | 'SessionTimer', 18 | ], 19 | }, 20 | }); 21 | 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/layout/innkeeper/__snapshots__/Header.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Header > mount matches snapshot with expected values 1`] = ` 4 | "" 18 | `; 19 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/components/messages/createMessage/CreateMessage.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import PrimeVue from 'primevue/config'; 3 | import { describe, expect, test } from 'vitest'; 4 | 5 | import CreateMessage from '@/components/messages/createMessage/CreateMessage.vue'; 6 | 7 | const mountCreateMessage = () => 8 | mount(CreateMessage, { 9 | global: { 10 | plugins: [PrimeVue], 11 | stubs: ['Dialog'], 12 | }, 13 | }); 14 | 15 | describe('CreateMessage', () => { 16 | test('mount matches expected values', async () => { 17 | const wrapper = mountCreateMessage(); 18 | 19 | wrapper.getComponent({ name: 'Button', props: { icon: 'pi pi-envelope' } }); 20 | expect(wrapper.getComponent({ name: 'Dialog' }).attributes().visible).toBe( 21 | 'false' 22 | ); 23 | expect(wrapper.getComponent({ name: 'Dialog' }).attributes().modal).toBe( 24 | 'true' 25 | ); 26 | }); 27 | 28 | test('clicking span sets dialog to visible', async () => { 29 | const wrapper = mountCreateMessage(); 30 | 31 | await wrapper 32 | .getComponent({ name: 'Button', props: { icon: 'pi pi-envelope' } }) 33 | .trigger('click'); 34 | 35 | expect(wrapper.getComponent({ name: 'Dialog' }).attributes().visible).toBe( 36 | 'true' 37 | ); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/store/commonStore.test.ts: -------------------------------------------------------------------------------- 1 | import { createPinia, setActivePinia } from 'pinia'; 2 | import { beforeEach, describe, expect, test } from 'vitest'; 3 | 4 | import { useCommonStore } from '@/store/commonStore'; 5 | 6 | describe('commonStore', () => { 7 | beforeEach(async () => { 8 | setActivePinia(createPinia()); 9 | }); 10 | 11 | test('initial values have not changed from expected', () => { 12 | const store = useCommonStore(); 13 | expect(store).toBeDefined(); 14 | expect(store.cardExpanded).toBe(false); 15 | expect(store.sidebarOpen).toBeNull(); 16 | }); 17 | 18 | test('sidebarOpenClass returns open when sidebarOpen is truthy', () => { 19 | const store = useCommonStore(); 20 | store.sidebarOpen = true; 21 | 22 | expect(store.sidebarOpenClass).toBe('open'); 23 | }); 24 | 25 | test('sidebarOpenClass returns clased when sidebarOpen is falsy', () => { 26 | const store = useCommonStore(); 27 | store.sidebarOpen = false; 28 | 29 | expect(store.sidebarOpenClass).toBe('closed'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/test/store/innkeeper/innkeeperOidcStore.test.ts: -------------------------------------------------------------------------------- 1 | import { createPinia, setActivePinia } from 'pinia'; 2 | import { beforeEach, describe, test } from 'vitest'; 3 | 4 | import { useInnkeeperOidcStore } from '@/store/innkeeper/innkeeperOidcStore'; 5 | 6 | describe('innkeeperOidcStore', () => { 7 | beforeEach(async () => { 8 | setActivePinia(createPinia()); 9 | useInnkeeperOidcStore(); 10 | }); 11 | 12 | describe('Successful API calls', () => { 13 | test.todo('login'); 14 | }); 15 | 16 | describe('Unsuccessful API calls', () => { 17 | test.todo('login'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["ESNext", "DOM"], 14 | "skipLibCheck": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "@/*": ["src/*"] 18 | }, 19 | "types": ["vite/client"] 20 | }, 21 | "include": [ 22 | "src/**/*.ts", 23 | "src/**/*.d.ts", 24 | "src/**/*.tsx", 25 | "src/**/*.vue", 26 | "src/components/holder/credentialOcaCard/OcaStyleConstants.ts", 27 | "test/**/*.ts" 28 | ], 29 | "references": [ 30 | { 31 | "path": "./tsconfig.node.json" 32 | } 33 | ], 34 | "exclude": ["node_modules"] 35 | } 36 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/tenant-ui/frontend/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue'; 3 | const component: DefineComponent< 4 | Record, 5 | Record, 6 | any 7 | >; 8 | export default component; 9 | } 10 | -------------------------------------------------------------------------------- /services/tenant-ui/src/components/email_templates/README.md: -------------------------------------------------------------------------------- 1 | # Email Templates 2 | 3 | Emails are sent to possible tenants when they: 4 | 5 | 1. Request a reservation 6 | 1. Get approved as a tenant. 7 | 1. Get denied as a tenant. 8 | 9 | In addition the Innkeeper receives an email when a tenant: 10 | 11 | 1. Requests a reservation. 12 | 13 | The email templates are located in the `email_templates` directory. The files are written in [EJS](https://ejs.co/), a templating language that is used to generate HTML. Each file is compiled into valid HTML, hydrated with data and sent to the user as required. 14 | 15 | In general, HTML intended for email should be written in a very simple and old school way. This is because email clients are very inconsistent in how they render HTML. For example, some email clients will not render CSS at all. For this reason, the email templates are written as if HTML 5 didn't exist.. Or even HTML 4 for that matter. The email templates are not intended to be pretty, but rather functional. 16 | 17 | In the future, if more complex email templates are needed, we may want to consider using a tool like [mjml](https://mjml.io/).] or possibly a service such as [Mailchimp](https://mailchimp.com/). 18 | -------------------------------------------------------------------------------- /services/tenant-ui/src/components/email_templates/reservation_approved_tenant.ts: -------------------------------------------------------------------------------- 1 | export const RESERVATION_APPROVED_TENANT_TEMPLATE = ` 2 |

3 | Hello <%= it.body.contactName _%>,
4 | Your Reservation Number (<%= it.body.reservationId _%>) has been updated. 5 |

6 |

7 | We are pleased to inform you that your reservation request is approved. Here 8 | is the Reservation Password that is required to validate your account: 9 | <%= it.body.reservationPassword _%>

10 |

11 | Please use this link to validate your account: 12 | <%= it.body.serverUrlStatusRoute _%> 13 |

14 |

15 | After successful validation of your account, a new Wallet ID and Wallet Key 16 | will be generated. Please save the newly generated Wallet ID and Wallet Key in 17 | a secure location as we never share this information over email nor do we 18 | re-issue upon request. 19 |

20 |

21 | Traction will never contact you to ask for details about your login 22 | credentials. 23 |

24 |

25 | Please do not forward this e-mail as it contains private information intended 26 | only for you. Please do not reply to this e-mail. 27 |

28 |

29 | Best regards
30 | Traction Team 31 |

32 | `; 33 | -------------------------------------------------------------------------------- /services/tenant-ui/src/components/email_templates/reservation_declined_tenant.ts: -------------------------------------------------------------------------------- 1 | export const RESERVATION_DECLINED_TENANT_TEMPLATE = ` 2 |

3 | Hello <%= it.body.contactName _%>,
4 | Your Reservation Number (<%= it.body.reservationId _%>) has been updated. 5 |

6 |

7 | Thank you for your interest in joining Traction. We regret to inform you that 8 | your request has been declined. 9 |

10 |

11 | Reason for rejection:
12 |   <%= it.body.stateNotes _%> 13 |

14 |

15 | If you think there has been an error, you can submit a new request here: <%= 16 | it.body.serverUrl _%> 17 |

18 |

19 | Please do not forward this email as it containes private information intended 20 | only for you. Please do not reply to this email. 21 |

22 |

23 | Best regards
24 | Traction Team 25 |

26 | `; 27 | -------------------------------------------------------------------------------- /services/tenant-ui/src/components/email_templates/reservation_received_innkeeper.ts: -------------------------------------------------------------------------------- 1 | export const RESERVATION_RECIEVED_INNKEEPER_TEMPLATE = ` 2 |

3 | Hello Innkeeper,
4 | This email is to notify you that we have received a request from <%= 5 | it.body.contactName _%>   (<%= it.body.contactEmail _%>) with reservation 6 | ID: 7 | <%= it.body.reservationId _%>. 8 |

9 |

10 | Please login to review and take action on the request: <%= it.body.serverUrl 11 | _%>/innkeeper 12 |

13 |

14 | Please do not forward this email as it contains private information intended 15 | only for you. Please do not reply to this email. 16 |

17 |

18 | Best regards
19 | Traction Team 20 |

21 | `; 22 | -------------------------------------------------------------------------------- /services/tenant-ui/src/components/email_templates/reservation_received_tenant.ts: -------------------------------------------------------------------------------- 1 | export const RESERVATION_RECIEVED_TENANT_TEMPLATE = ` 2 |

3 | Hello <%= it.body.contactName _%>,
Thank you for your request to join 4 | Traction 5 |

6 |

7 | This email confirms we have received your request and a Representative is 8 | working on your case.
9 | Your Reservation Number is <%= it.body.reservationId _%>. 10 |

11 |

12 | A decision on the request usually takes 2-3 days from the date of this email. 13 | You can verify the status of your request by entering your email and 14 | Reservation Number here: <%= it.body.serverUrlStatusRoute _%> 15 |

16 |

17 | Please do not forward this email as it contains private information intended 18 | only for you. Please do not reply to this email. 19 |

20 |

Thank you for choosing Traction!

21 |

22 | Best regards
23 | Traction Team 24 |

25 | `; 26 | -------------------------------------------------------------------------------- /services/tenant-ui/src/components/innkeeper.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import config from "config"; 3 | const TRACTION_URL: string = config.get("server.tractionUrl"); 4 | const INNKEEPER_USER = config.get("server.innkeeper.user"); 5 | const INNKEEPER_KEY = config.get("server.innkeeper.key"); 6 | 7 | /** 8 | * @function login 9 | * Use the configured Inkeeper Admin key to get the token 10 | * @returns {string} The inkeeper token 11 | */ 12 | export const login = async () => { 13 | const loginUrl = `${TRACTION_URL}/multitenancy/tenant/${INNKEEPER_USER}/token`; 14 | const payload = { wallet_key: INNKEEPER_KEY }; 15 | const res = await axios({ 16 | method: "post", 17 | url: loginUrl, 18 | data: payload, 19 | }); 20 | 21 | return res.data; 22 | }; 23 | 24 | /** 25 | * @function createReservation 26 | * Create a reservation in Traction 27 | * @returns {object} the reservation object 28 | */ 29 | export const createReservation = async (req: any, token: string) => { 30 | try { 31 | const auth = `Bearer ${token}`; 32 | const reservationUrl = `${TRACTION_URL}/innkeeper/reservations`; 33 | const payload = req.body; 34 | 35 | const res = await axios({ 36 | method: "post", 37 | url: reservationUrl, 38 | data: payload, 39 | headers: { 40 | Authorization: auth, 41 | }, 42 | }); 43 | return res.data; 44 | } catch (error) { 45 | return error; 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /services/tenant-ui/src/helpers/constants.ts: -------------------------------------------------------------------------------- 1 | export const RESERVATION_STATUSES = { 2 | APPROVED: "approved", 3 | CHECKED_IN: "checked_in", 4 | DENIED: "denied", 5 | REQUESTED: "requested", 6 | } as const; 7 | 8 | export type ReservationStatus = 9 | (typeof RESERVATION_STATUSES)[keyof typeof RESERVATION_STATUSES]; 10 | -------------------------------------------------------------------------------- /services/tenant-ui/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @function buildStatusAutofill 3 | * Format the reservation status return url with autofil params 4 | * @param {String} value The body from the email endpoint 5 | * @returns {String} The URL (env/check-status?email=X&id=Y) 6 | */ 7 | export function buildStatusAutofill(requestBody: any) { 8 | if (requestBody && requestBody.serverUrlStatusRoute) { 9 | return encodeURI( 10 | `${requestBody.serverUrlStatusRoute}?email=${requestBody.contactEmail}&id=${requestBody.reservationId}` 11 | ); 12 | } else { 13 | return ""; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /services/tenant-ui/src/helpers/startbanner.ts: -------------------------------------------------------------------------------- 1 | interface StartupBannerVals { 2 | port: number; 3 | apiRoot: string; 4 | staticPath: string; 5 | lokiUrl?: string; 6 | } 7 | 8 | export function logStartupBanner(vals: StartupBannerVals): void { 9 | // Print a startup banner with server details 10 | const env = process.env.NODE_ENV || "development"; 11 | console.log(""); 12 | console.log("🔷 Server Startup"); 13 | console.log(`- Mode: ${env}`); 14 | console.log(`- API Root: ${vals.apiRoot}`); 15 | console.log(`- Port: ${vals.port}`); 16 | console.log(`- Tenant UI FE dist: ${vals.staticPath}`); 17 | if (vals.lokiUrl) { 18 | console.log(`- Loki URL: ${vals.lokiUrl}`); 19 | } 20 | console.log("🚀 Tenant UI Backend is up 🚀"); 21 | console.log(""); 22 | } 23 | -------------------------------------------------------------------------------- /services/tenant-ui/src/middleware/oidcMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { createRemoteJWKSet, jwtVerify, JWTPayload } from "jose"; 2 | import config from "config"; 3 | import { Request, Response, NextFunction } from "express"; 4 | 5 | // Extend Express Request to include claims 6 | interface AuthenticatedRequest extends Request { 7 | claims?: JWTPayload; 8 | } 9 | 10 | const jwksUri = new URL(config.get("server.oidc.jwksUri")); 11 | const jwks = createRemoteJWKSet(jwksUri); 12 | 13 | const oidcMiddleware = async ( 14 | req: AuthenticatedRequest, 15 | res: Response, 16 | next: NextFunction 17 | ) => { 18 | const authHeader = req.headers.authorization; 19 | if (!authHeader || !authHeader.startsWith("Bearer ")) { 20 | res.status(401).json({ message: "Unauthorized" }); 21 | return; 22 | } 23 | const token = authHeader.split(" ")[1]; 24 | try { 25 | const { payload } = await jwtVerify(token, jwks); 26 | req.claims = payload; 27 | next(); 28 | } catch (error) { 29 | res.status(401).json({ message: "Invalid token", error }); 30 | } 31 | }; 32 | 33 | export default oidcMiddleware; 34 | -------------------------------------------------------------------------------- /services/tenant-ui/test/components/email_templates/email-templates.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Place holder for email-templates tests 3 | */ 4 | test("my first test", () => { 5 | expect(true).toBe(true); 6 | }); 7 | -------------------------------------------------------------------------------- /services/tenant-ui/test/helpers/constants.spec.ts: -------------------------------------------------------------------------------- 1 | import { RESERVATION_STATUSES } from "../../src/helpers/constants.js"; 2 | 3 | describe("constants", () => { 4 | describe("RESERVATION_STATUSES", () => { 5 | it("should have all expected status values", () => { 6 | expect(RESERVATION_STATUSES.APPROVED).toBe("approved"); 7 | expect(RESERVATION_STATUSES.CHECKED_IN).toBe("checked_in"); 8 | expect(RESERVATION_STATUSES.DENIED).toBe("denied"); 9 | expect(RESERVATION_STATUSES.REQUESTED).toBe("requested"); 10 | }); 11 | 12 | it("should have correct number of statuses", () => { 13 | expect(Object.keys(RESERVATION_STATUSES)).toHaveLength(4); 14 | }); 15 | 16 | it("should have immutable structure", () => { 17 | const statusKeys = Object.keys(RESERVATION_STATUSES); 18 | expect(statusKeys).toEqual([ 19 | "APPROVED", 20 | "CHECKED_IN", 21 | "DENIED", 22 | "REQUESTED", 23 | ]); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /services/tenant-ui/test/helpers/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { buildStatusAutofill } from "../../src/helpers/index.js"; 2 | 3 | describe("buildStatusAutofill", () => { 4 | const body = { 5 | serverUrlStatusRoute: "http://tenant-ui.gov.bc.ca", 6 | contactEmail: "my.email@gov.fake", 7 | reservationId: "50542cfe-e9bd-4881-8a71-1b529f40bc2b", 8 | }; 9 | 10 | it("should return a correctly formed url including the body params", () => { 11 | expect(buildStatusAutofill(body)).toBe( 12 | "http://tenant-ui.gov.bc.ca?email=my.email@gov.fake&id=50542cfe-e9bd-4881-8a71-1b529f40bc2b" 13 | ); 14 | }); 15 | 16 | it("should return a blank string for no input param", () => { 17 | expect(buildStatusAutofill(undefined)).toBe(""); 18 | expect(buildStatusAutofill(null)).toBe(""); 19 | expect(buildStatusAutofill({})).toBe(""); 20 | expect(buildStatusAutofill("")).toBe(""); 21 | }); 22 | 23 | it("should return a blank string for no serverUrlStatusRoute", () => { 24 | expect( 25 | buildStatusAutofill({ 26 | contactEmail: "abc@test.com", 27 | reservationId: "123", 28 | }) 29 | ).toBe(""); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /services/tenant-ui/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": {}, 5 | "rules": { 6 | "trailing-comma": false, 7 | "object-literal-sort-keys": false, 8 | "object-literal-key-quotes": false, 9 | "ordered-imports": false, 10 | "interface-name": false, 11 | "quotemark": false, 12 | "no-console": false, 13 | "eofline": false, 14 | "typedef": false 15 | }, 16 | "rulesDirectory": [] 17 | } 18 | -------------------------------------------------------------------------------- /services/tenant-ui/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: "node", 7 | include: ["test/**/*.{test,spec}.{js,ts}"], 8 | coverage: { 9 | provider: "v8", 10 | include: ["src/**/*.{js,ts}"], 11 | exclude: ["src/**/*.d.ts", "src/**/*.config.{js,ts}"], 12 | reporter: ["text", "lcov", "html"], 13 | reportsDirectory: "./coverage", 14 | }, 15 | }, 16 | }); 17 | --------------------------------------------------------------------------------