ddd-ts-kit 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/dist/check/explain/tenancy-docs.d.ts.map +1 -1
- package/dist/check/explain/tenancy-docs.js +3 -1
- package/dist/check/explain/tenancy-docs.js.map +1 -1
- package/dist/check/fixtures/tenancy-fixtures.d.ts.map +1 -1
- package/dist/check/fixtures/tenancy-fixtures.js +28 -0
- package/dist/check/fixtures/tenancy-fixtures.js.map +1 -1
- package/dist/check/tenancy/knex-unscoped.d.ts +6 -1
- package/dist/check/tenancy/knex-unscoped.d.ts.map +1 -1
- package/dist/check/tenancy/knex-unscoped.js +19 -6
- package/dist/check/tenancy/knex-unscoped.js.map +1 -1
- package/dist/check/tenancy/raw-reaches-rows.d.ts +8 -0
- package/dist/check/tenancy/raw-reaches-rows.d.ts.map +1 -0
- package/dist/check/tenancy/raw-reaches-rows.js +91 -0
- package/dist/check/tenancy/raw-reaches-rows.js.map +1 -0
- package/dist/check/tenancy/raw-sql-text.d.ts +9 -0
- package/dist/check/tenancy/raw-sql-text.d.ts.map +1 -0
- package/dist/check/tenancy/raw-sql-text.js +29 -0
- package/dist/check/tenancy/raw-sql-text.js.map +1 -0
- package/dist/commands/generate.js +2 -0
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +11 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/declared/injection/laid-out-calls.d.ts +7 -0
- package/dist/declared/injection/laid-out-calls.d.ts.map +1 -0
- package/dist/declared/injection/laid-out-calls.js +14 -0
- package/dist/declared/injection/laid-out-calls.js.map +1 -0
- package/dist/declared/render-barrel.d.ts.map +1 -1
- package/dist/declared/render-barrel.js +2 -1
- package/dist/declared/render-barrel.js.map +1 -1
- package/dist/declared/render-dependencies-file.d.ts.map +1 -1
- package/dist/declared/render-dependencies-file.js +2 -1
- package/dist/declared/render-dependencies-file.js.map +1 -1
- package/dist/generate/kind.d.ts +2 -0
- package/dist/generate/kind.d.ts.map +1 -1
- package/dist/generate/kinds/context-kind.d.ts.map +1 -1
- package/dist/generate/kinds/context-kind.js +13 -6
- package/dist/generate/kinds/context-kind.js.map +1 -1
- package/dist/generate/kinds/next-adr-number.d.ts +4 -0
- package/dist/generate/kinds/next-adr-number.d.ts.map +1 -0
- package/dist/generate/kinds/next-adr-number.js +7 -0
- package/dist/generate/kinds/next-adr-number.js.map +1 -0
- package/dist/generate/kinds/write-adr.js +3 -6
- package/dist/generate/kinds/write-adr.js.map +1 -1
- package/dist/help/usage.d.ts +1 -1
- package/dist/help/usage.d.ts.map +1 -1
- package/dist/help/usage.js +1 -1
- package/dist/init/manifest/versions.js +24 -24
- package/dist/init/run-init.d.ts +2 -0
- package/dist/init/run-init.d.ts.map +1 -1
- package/dist/init/run-init.js +15 -1
- package/dist/init/run-init.js.map +1 -1
- package/dist/presets/apply-preset.d.ts +15 -0
- package/dist/presets/apply-preset.d.ts.map +1 -0
- package/dist/presets/apply-preset.js +94 -0
- package/dist/presets/apply-preset.js.map +1 -0
- package/dist/presets/grant-to-signed-in.d.ts +9 -0
- package/dist/presets/grant-to-signed-in.d.ts.map +1 -0
- package/dist/presets/grant-to-signed-in.js +61 -0
- package/dist/presets/grant-to-signed-in.js.map +1 -0
- package/dist/presets/identity-preset.d.ts +9 -0
- package/dist/presets/identity-preset.d.ts.map +1 -0
- package/dist/presets/identity-preset.js +44 -0
- package/dist/presets/identity-preset.js.map +1 -0
- package/dist/presets/identity-refusal.d.ts +5 -0
- package/dist/presets/identity-refusal.d.ts.map +1 -0
- package/dist/presets/identity-refusal.js +29 -0
- package/dist/presets/identity-refusal.js.map +1 -0
- package/dist/presets/identity-terms.d.ts +4 -0
- package/dist/presets/identity-terms.d.ts.map +1 -0
- package/dist/presets/identity-terms.js +57 -0
- package/dist/presets/identity-terms.js.map +1 -0
- package/dist/presets/no-flags.d.ts +4 -0
- package/dist/presets/no-flags.d.ts.map +1 -0
- package/dist/presets/no-flags.js +18 -0
- package/dist/presets/no-flags.js.map +1 -0
- package/dist/presets/organisations-as-tenants.d.ts +9 -0
- package/dist/presets/organisations-as-tenants.d.ts.map +1 -0
- package/dist/presets/organisations-as-tenants.js +43 -0
- package/dist/presets/organisations-as-tenants.js.map +1 -0
- package/dist/presets/preset-decision.d.ts +8 -0
- package/dist/presets/preset-decision.d.ts.map +1 -0
- package/dist/presets/preset-decision.js +19 -0
- package/dist/presets/preset-decision.js.map +1 -0
- package/dist/presets/preset-file.d.ts +13 -0
- package/dist/presets/preset-file.d.ts.map +1 -0
- package/dist/presets/preset-file.js +2 -0
- package/dist/presets/preset-file.js.map +1 -0
- package/dist/presets/preset-files.d.ts +9 -0
- package/dist/presets/preset-files.d.ts.map +1 -0
- package/dist/presets/preset-files.js +34 -0
- package/dist/presets/preset-files.js.map +1 -0
- package/dist/presets/preset-named.d.ts +3 -0
- package/dist/presets/preset-named.d.ts.map +1 -0
- package/dist/presets/preset-named.js +12 -0
- package/dist/presets/preset-named.js.map +1 -0
- package/dist/presets/preset-refusal.d.ts +6 -0
- package/dist/presets/preset-refusal.d.ts.map +1 -0
- package/dist/presets/preset-refusal.js +2 -0
- package/dist/presets/preset-refusal.js.map +1 -0
- package/dist/presets/preset-step.d.ts +8 -0
- package/dist/presets/preset-step.d.ts.map +1 -0
- package/dist/presets/preset-step.js +2 -0
- package/dist/presets/preset-step.js.map +1 -0
- package/dist/presets/preset.d.ts +36 -0
- package/dist/presets/preset.d.ts.map +1 -0
- package/dist/presets/preset.js +2 -0
- package/dist/presets/preset.js.map +1 -0
- package/dist/presets/with-authorizer-import.d.ts +6 -0
- package/dist/presets/with-authorizer-import.d.ts.map +1 -0
- package/dist/presets/with-authorizer-import.js +27 -0
- package/dist/presets/with-authorizer-import.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace/workspace.d.ts +2 -0
- package/dist/workspace/workspace.d.ts.map +1 -1
- package/dist/workspace/workspace.js +4 -0
- package/dist/workspace/workspace.js.map +1 -1
- package/package.json +2 -2
- package/templates/pipeline/package-scripts.json +1 -1
- package/templates/presets/identity/context/application/authorization/__context__.permissions.ts.tpl +23 -0
- package/templates/presets/identity/context/application/authorization/change-member-roles.policy.ts.tpl +17 -0
- package/templates/presets/identity/context/application/authorization/grant-delegations.policy.ts.tpl +17 -0
- package/templates/presets/identity/context/application/authorization/invite-members.policy.ts.tpl +17 -0
- package/templates/presets/identity/context/application/authorization/read-members.policy.ts.tpl +17 -0
- package/templates/presets/identity/context/application/authorization/remove-members.policy.ts.tpl +17 -0
- package/templates/presets/identity/context/application/authorization/revoke-delegations.policy.ts.tpl +17 -0
- package/templates/presets/identity/context/application/authorization/revoke-invitations.policy.ts.tpl +17 -0
- package/templates/presets/identity/context/application/commands/accept-invitation/accept-invitation.command.ts.tpl +14 -0
- package/templates/presets/identity/context/application/commands/accept-invitation/accept-invitation.handler.ts.tpl +56 -0
- package/templates/presets/identity/context/application/commands/change-member-role/change-member-role.command.ts.tpl +17 -0
- package/templates/presets/identity/context/application/commands/change-member-role/change-member-role.handler.ts.tpl +82 -0
- package/templates/presets/identity/context/application/commands/deactivate-account/deactivate-account.command.ts.tpl +7 -0
- package/templates/presets/identity/context/application/commands/deactivate-account/deactivate-account.handler.ts.tpl +69 -0
- package/templates/presets/identity/context/application/commands/decline-invitation/decline-invitation.command.ts.tpl +14 -0
- package/templates/presets/identity/context/application/commands/decline-invitation/decline-invitation.handler.ts.tpl +36 -0
- package/templates/presets/identity/context/application/commands/grant-delegation/grant-delegation.command.ts.tpl +21 -0
- package/templates/presets/identity/context/application/commands/grant-delegation/grant-delegation.handler.ts.tpl +43 -0
- package/templates/presets/identity/context/application/commands/invite-member/invite-member.command.ts.tpl +16 -0
- package/templates/presets/identity/context/application/commands/invite-member/invite-member.handler.ts.tpl +53 -0
- package/templates/presets/identity/context/application/commands/open-organisation/open-organisation.command.ts.tpl +14 -0
- package/templates/presets/identity/context/application/commands/open-organisation/open-organisation.handler.ts.tpl +50 -0
- package/templates/presets/identity/context/application/commands/provision-account/provision-account.command.ts.tpl +14 -0
- package/templates/presets/identity/context/application/commands/provision-account/provision-account.handler.ts.tpl +41 -0
- package/templates/presets/identity/context/application/commands/remove-member/remove-member.command.ts.tpl +15 -0
- package/templates/presets/identity/context/application/commands/remove-member/remove-member.handler.ts.tpl +79 -0
- package/templates/presets/identity/context/application/commands/revoke-delegation/revoke-delegation.command.ts.tpl +15 -0
- package/templates/presets/identity/context/application/commands/revoke-delegation/revoke-delegation.handler.ts.tpl +39 -0
- package/templates/presets/identity/context/application/commands/revoke-invitation/revoke-invitation.command.ts.tpl +15 -0
- package/templates/presets/identity/context/application/commands/revoke-invitation/revoke-invitation.handler.ts.tpl +42 -0
- package/templates/presets/identity/context/application/integration/handlers/account-deactivated.integration-handler.ts.tpl +19 -0
- package/templates/presets/identity/context/application/integration/handlers/member-joined.integration-handler.ts.tpl +33 -0
- package/templates/presets/identity/context/application/integration/handlers/member-removed.integration-handler.ts.tpl +30 -0
- package/templates/presets/identity/context/application/integration/handlers/organisation-opened.integration-handler.ts.tpl +19 -0
- package/templates/presets/identity/context/application/integration/publishers/account-deactivated.publisher.ts.tpl +24 -0
- package/templates/presets/identity/context/application/integration/publishers/delegation-granted.publisher.ts.tpl +36 -0
- package/templates/presets/identity/context/application/integration/publishers/delegation-revoked.publisher.ts.tpl +33 -0
- package/templates/presets/identity/context/application/integration/publishers/member-joined.publisher.ts.tpl +33 -0
- package/templates/presets/identity/context/application/integration/publishers/member-removed.publisher.ts.tpl +33 -0
- package/templates/presets/identity/context/application/integration/publishers/member-role-changed.publisher.ts.tpl +35 -0
- package/templates/presets/identity/context/application/integration/publishers/organisation-opened.publisher.ts.tpl +32 -0
- package/templates/presets/identity/context/application/ports/identity-provider-admin.port.ts.tpl +17 -0
- package/templates/presets/identity/context/application/ports/identity-provider-connection.port.ts.tpl +14 -0
- package/templates/presets/identity/context/application/ports/invitation-tokens.port.ts.tpl +7 -0
- package/templates/presets/identity/context/application/ports/search-members-reader.port.ts.tpl +10 -0
- package/templates/presets/identity/context/application/queries/search-members/search-members.handler.ts.tpl +33 -0
- package/templates/presets/identity/context/application/queries/search-members/search-members.query.ts.tpl +19 -0
- package/templates/presets/identity/context/application/queries/search-members/search-members.result.ts.tpl +17 -0
- package/templates/presets/identity/context/domain/aggregates/account.aggregate.ts.tpl +92 -0
- package/templates/presets/identity/context/domain/aggregates/delegation.aggregate.ts.tpl +117 -0
- package/templates/presets/identity/context/domain/aggregates/invitation.aggregate.ts.tpl +128 -0
- package/templates/presets/identity/context/domain/aggregates/member.aggregate.ts.tpl +149 -0
- package/templates/presets/identity/context/domain/aggregates/organisation.aggregate.ts.tpl +52 -0
- package/templates/presets/identity/context/domain/errors/account.error.ts.tpl +40 -0
- package/templates/presets/identity/context/domain/errors/delegation.error.ts.tpl +45 -0
- package/templates/presets/identity/context/domain/errors/invitation.error.ts.tpl +30 -0
- package/templates/presets/identity/context/domain/errors/member.error.ts.tpl +50 -0
- package/templates/presets/identity/context/domain/errors/organisation.error.ts.tpl +32 -0
- package/templates/presets/identity/context/domain/events/account-deactivated.event.ts.tpl +13 -0
- package/templates/presets/identity/context/domain/events/account-provisioned.event.ts.tpl +17 -0
- package/templates/presets/identity/context/domain/events/delegation-granted.event.ts.tpl +22 -0
- package/templates/presets/identity/context/domain/events/delegation-revoked.event.ts.tpl +18 -0
- package/templates/presets/identity/context/domain/events/invitation-accepted.event.ts.tpl +17 -0
- package/templates/presets/identity/context/domain/events/invitation-declined.event.ts.tpl +16 -0
- package/templates/presets/identity/context/domain/events/invitation-issued.event.ts.tpl +22 -0
- package/templates/presets/identity/context/domain/events/invitation-revoked.event.ts.tpl +16 -0
- package/templates/presets/identity/context/domain/events/member-joined.event.ts.tpl +19 -0
- package/templates/presets/identity/context/domain/events/member-removed.event.ts.tpl +19 -0
- package/templates/presets/identity/context/domain/events/member-role-changed.event.ts.tpl +22 -0
- package/templates/presets/identity/context/domain/events/organisation-opened.event.ts.tpl +17 -0
- package/templates/presets/identity/context/domain/repositories/account.repository.ts.tpl +5 -0
- package/templates/presets/identity/context/domain/repositories/delegation.repository.ts.tpl +10 -0
- package/templates/presets/identity/context/domain/repositories/invitation.repository.ts.tpl +7 -0
- package/templates/presets/identity/context/domain/repositories/member.repository.ts.tpl +13 -0
- package/templates/presets/identity/context/domain/repositories/organisation.repository.ts.tpl +5 -0
- package/templates/presets/identity/context/domain/services/role-rights.service.ts.tpl +59 -0
- package/templates/presets/identity/context/domain/value-objects/delegation-period.vo.ts.tpl +42 -0
- package/templates/presets/identity/context/domain/value-objects/display-name.vo.ts.tpl +29 -0
- package/templates/presets/identity/context/domain/value-objects/email-address.vo.ts.tpl +31 -0
- package/templates/presets/identity/context/domain/value-objects/organisation-name.vo.ts.tpl +29 -0
- package/templates/presets/identity/context/domain/value-objects/organisation-rights.vo.ts.tpl +43 -0
- package/templates/presets/identity/context/domain/value-objects/organisation-slug.vo.ts.tpl +30 -0
- package/templates/presets/identity/context/domain/vocabularies/account-status.vocabulary.ts.tpl +7 -0
- package/templates/presets/identity/context/domain/vocabularies/invitation-status.vocabulary.ts.tpl +9 -0
- package/templates/presets/identity/context/domain/vocabularies/member-right.vocabulary.ts.tpl +9 -0
- package/templates/presets/identity/context/domain/vocabularies/member-role.vocabulary.ts.tpl +8 -0
- package/templates/presets/identity/context/infrastructure/external/crypto-invitation-tokens.adapter.ts.tpl +20 -0
- package/templates/presets/identity/context/infrastructure/external/environment-identity-provider-connection.adapter.ts.tpl +37 -0
- package/templates/presets/identity/context/infrastructure/external/keycloak-identity-provider-admin.adapter.ts.tpl +149 -0
- package/templates/presets/identity/context/infrastructure/persistence/__context__.schema.drizzle.ts.tpl +102 -0
- package/templates/presets/identity/context/infrastructure/persistence/account-row.schema.knex.ts.tpl +15 -0
- package/templates/presets/identity/context/infrastructure/persistence/account.mapper.drizzle.ts.tpl +38 -0
- package/templates/presets/identity/context/infrastructure/persistence/account.mapper.knex.ts.tpl +33 -0
- package/templates/presets/identity/context/infrastructure/persistence/create-__context__-tables.schema.knex.ts.tpl +107 -0
- package/templates/presets/identity/context/infrastructure/persistence/delegation-row.schema.knex.ts.tpl +17 -0
- package/templates/presets/identity/context/infrastructure/persistence/delegation.mapper.drizzle.ts.tpl +50 -0
- package/templates/presets/identity/context/infrastructure/persistence/delegation.mapper.knex.ts.tpl +45 -0
- package/templates/presets/identity/context/infrastructure/persistence/drizzle-account.repository.drizzle.ts.tpl +59 -0
- package/templates/presets/identity/context/infrastructure/persistence/drizzle-delegation.repository.drizzle.ts.tpl +77 -0
- package/templates/presets/identity/context/infrastructure/persistence/drizzle-invitation.repository.drizzle.ts.tpl +65 -0
- package/templates/presets/identity/context/infrastructure/persistence/drizzle-member.repository.drizzle.ts.tpl +84 -0
- package/templates/presets/identity/context/infrastructure/persistence/drizzle-organisation.repository.drizzle.ts.tpl +59 -0
- package/templates/presets/identity/context/infrastructure/persistence/drizzle-search-members-reader.repository.drizzle.ts.tpl +53 -0
- package/templates/presets/identity/context/infrastructure/persistence/invitation-row.schema.knex.ts.tpl +20 -0
- package/templates/presets/identity/context/infrastructure/persistence/invitation.mapper.drizzle.ts.tpl +45 -0
- package/templates/presets/identity/context/infrastructure/persistence/invitation.mapper.knex.ts.tpl +40 -0
- package/templates/presets/identity/context/infrastructure/persistence/knex-account.repository.knex.ts.tpl +61 -0
- package/templates/presets/identity/context/infrastructure/persistence/knex-delegation.repository.knex.ts.tpl +80 -0
- package/templates/presets/identity/context/infrastructure/persistence/knex-invitation.repository.knex.ts.tpl +67 -0
- package/templates/presets/identity/context/infrastructure/persistence/knex-member.repository.knex.ts.tpl +82 -0
- package/templates/presets/identity/context/infrastructure/persistence/knex-organisation.repository.knex.ts.tpl +61 -0
- package/templates/presets/identity/context/infrastructure/persistence/knex-search-members-reader.repository.knex.ts.tpl +77 -0
- package/templates/presets/identity/context/infrastructure/persistence/member-row.schema.knex.ts.tpl +15 -0
- package/templates/presets/identity/context/infrastructure/persistence/member.mapper.drizzle.ts.tpl +36 -0
- package/templates/presets/identity/context/infrastructure/persistence/member.mapper.knex.ts.tpl +31 -0
- package/templates/presets/identity/context/infrastructure/persistence/organisation-row.schema.knex.ts.tpl +12 -0
- package/templates/presets/identity/context/infrastructure/persistence/organisation.mapper.drizzle.ts.tpl +28 -0
- package/templates/presets/identity/context/infrastructure/persistence/organisation.mapper.knex.ts.tpl +26 -0
- package/templates/presets/identity/context/presentation/http/accounts/deactivate-account/deactivate-account.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/accounts/deactivate-account/deactivate-account.controller.ts.tpl +71 -0
- package/templates/presets/identity/context/presentation/http/accounts/provision-account/provision-account.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/accounts/provision-account/provision-account.body.class-validator.ts.tpl +9 -0
- package/templates/presets/identity/context/presentation/http/accounts/provision-account/provision-account.controller.ts.tpl +85 -0
- package/templates/presets/identity/context/presentation/http/accounts.routes.ts.tpl +5 -0
- package/templates/presets/identity/context/presentation/http/invitations/accept-invitation/accept-invitation.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/invitations/accept-invitation/accept-invitation.body.class-validator.per-route.ts.tpl +8 -0
- package/templates/presets/identity/context/presentation/http/invitations/accept-invitation/accept-invitation.controller.ts.tpl +100 -0
- package/templates/presets/identity/context/presentation/http/invitations/decline-invitation/decline-invitation.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/invitations/decline-invitation/decline-invitation.body.class-validator.per-route.ts.tpl +8 -0
- package/templates/presets/identity/context/presentation/http/invitations/decline-invitation/decline-invitation.controller.ts.tpl +98 -0
- package/templates/presets/identity/context/presentation/http/invitations.routes.ts.tpl +5 -0
- package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.body.class-validator.ts.tpl +7 -0
- package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.controller.ts.tpl +83 -0
- package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.params.class-validator.ts.tpl +11 -0
- package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.body.class-validator.ts.tpl +18 -0
- package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.controller.ts.tpl +103 -0
- package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.params.class-validator.ts.tpl +7 -0
- package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.accepted.class-validator.ts.tpl +10 -0
- package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.body.class-validator.ts.tpl +10 -0
- package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.controller.ts.tpl +65 -0
- package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.params.class-validator.ts.tpl +7 -0
- package/templates/presets/identity/context/presentation/http/organisations/open-organisation/open-organisation.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/organisations/open-organisation/open-organisation.body.class-validator.ts.tpl +12 -0
- package/templates/presets/identity/context/presentation/http/organisations/open-organisation/open-organisation.controller.ts.tpl +79 -0
- package/templates/presets/identity/context/presentation/http/organisations/remove-member/remove-member.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/organisations/remove-member/remove-member.controller.ts.tpl +73 -0
- package/templates/presets/identity/context/presentation/http/organisations/remove-member/remove-member.params.class-validator.ts.tpl +11 -0
- package/templates/presets/identity/context/presentation/http/organisations/revoke-delegation/revoke-delegation.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/organisations/revoke-delegation/revoke-delegation.controller.ts.tpl +73 -0
- package/templates/presets/identity/context/presentation/http/organisations/revoke-delegation/revoke-delegation.params.class-validator.ts.tpl +11 -0
- package/templates/presets/identity/context/presentation/http/organisations/revoke-invitation/revoke-invitation.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/organisations/revoke-invitation/revoke-invitation.controller.ts.tpl +73 -0
- package/templates/presets/identity/context/presentation/http/organisations/revoke-invitation/revoke-invitation.params.class-validator.ts.tpl +11 -0
- package/templates/presets/identity/context/presentation/http/organisations/search-members/search-members.controller.ts.tpl +86 -0
- package/templates/presets/identity/context/presentation/http/organisations/search-members/search-members.line.class-validator.ts.tpl +22 -0
- package/templates/presets/identity/context/presentation/http/organisations/search-members/search-members.params.class-validator.ts.tpl +7 -0
- package/templates/presets/identity/context/presentation/http/organisations.routes.ts.tpl +5 -0
- package/templates/presets/identity/context/presentation/http/shared/invitation-token.schema.class-validator.shared.ts.tpl +9 -0
- package/templates/presets/identity/context/presentation/http/shared/invitation-token.schema.zod.shared.ts.tpl +9 -0
- package/templates/presets/identity/context/published-language/account-deactivated.v1.integration-event.ts.tpl +33 -0
- package/templates/presets/identity/context/published-language/account-deactivated.v1.payload.class-validator.ts.tpl +7 -0
- package/templates/presets/identity/context/published-language/delegation-granted.v1.integration-event.ts.tpl +39 -0
- package/templates/presets/identity/context/published-language/delegation-granted.v1.payload.class-validator.ts.tpl +28 -0
- package/templates/presets/identity/context/published-language/delegation-revoked.v1.integration-event.ts.tpl +36 -0
- package/templates/presets/identity/context/published-language/delegation-revoked.v1.payload.class-validator.ts.tpl +16 -0
- package/templates/presets/identity/context/published-language/member-joined.v1.integration-event.ts.tpl +36 -0
- package/templates/presets/identity/context/published-language/member-joined.v1.payload.class-validator.ts.tpl +16 -0
- package/templates/presets/identity/context/published-language/member-removed.v1.integration-event.ts.tpl +36 -0
- package/templates/presets/identity/context/published-language/member-removed.v1.payload.class-validator.ts.tpl +18 -0
- package/templates/presets/identity/context/published-language/member-role-changed.v1.integration-event.ts.tpl +38 -0
- package/templates/presets/identity/context/published-language/member-role-changed.v1.payload.class-validator.ts.tpl +24 -0
- package/templates/presets/identity/context/published-language/organisation-opened.v1.integration-event.ts.tpl +35 -0
- package/templates/presets/identity/context/published-language/organisation-opened.v1.payload.class-validator.ts.tpl +13 -0
- package/templates/presets/identity/context/test/builders/account.builder.ts.tpl +59 -0
- package/templates/presets/identity/context/test/builders/delegation.builder.ts.tpl +67 -0
- package/templates/presets/identity/context/test/builders/invitation.builder.ts.tpl +60 -0
- package/templates/presets/identity/context/test/builders/member.builder.ts.tpl +59 -0
- package/templates/presets/identity/context/test/builders/organisation.builder.ts.tpl +44 -0
- package/templates/presets/identity/context/test/fakes/account.repository.fake.ts.tpl +10 -0
- package/templates/presets/identity/context/test/fakes/delegation.repository.fake.ts.tpl +26 -0
- package/templates/presets/identity/context/test/fakes/identity-provider-admin.fake.ts.tpl +41 -0
- package/templates/presets/identity/context/test/fakes/invitation-tokens.fake.ts.tpl +18 -0
- package/templates/presets/identity/context/test/fakes/invitation.repository.fake.ts.tpl +14 -0
- package/templates/presets/identity/context/test/fakes/member.repository.fake.ts.tpl +29 -0
- package/templates/presets/identity/context/test/fakes/organisation.repository.fake.ts.tpl +10 -0
- package/templates/presets/identity/context/test/fakes/search-members-reader.fake.ts.tpl +25 -0
- package/templates/presets/identity/context/test/integration/account.repository.contract.spec.ts.tpl +48 -0
- package/templates/presets/identity/context/test/integration/delegation.repository.contract.spec.ts.tpl +84 -0
- package/templates/presets/identity/context/test/integration/invitation.repository.contract.spec.ts.tpl +76 -0
- package/templates/presets/identity/context/test/integration/keycloak-identity-provider-admin.adapter.spec.ts.tpl +123 -0
- package/templates/presets/identity/context/test/integration/member.repository.contract.spec.ts.tpl +92 -0
- package/templates/presets/identity/context/test/integration/organisation.repository.contract.spec.ts.tpl +46 -0
- package/templates/presets/identity/context/test/integration/search-members-reader.spec.ts.tpl +86 -0
- package/templates/presets/identity/context/test/unit/accept-invitation.controller.spec.ts.tpl +57 -0
- package/templates/presets/identity/context/test/unit/accept-invitation.handler.spec.ts.tpl +69 -0
- package/templates/presets/identity/context/test/unit/account-deactivated.integration-handler.spec.ts.tpl +24 -0
- package/templates/presets/identity/context/test/unit/account-deactivated.publisher.spec.ts.tpl +25 -0
- package/templates/presets/identity/context/test/unit/account.aggregate.spec.ts.tpl +72 -0
- package/templates/presets/identity/context/test/unit/change-member-role.controller.spec.ts.tpl +57 -0
- package/templates/presets/identity/context/test/unit/change-member-role.handler.spec.ts.tpl +135 -0
- package/templates/presets/identity/context/test/unit/crypto-invitation-tokens.adapter.spec.ts.tpl +15 -0
- package/templates/presets/identity/context/test/unit/deactivate-account.controller.spec.ts.tpl +44 -0
- package/templates/presets/identity/context/test/unit/deactivate-account.handler.spec.ts.tpl +106 -0
- package/templates/presets/identity/context/test/unit/decline-invitation.controller.spec.ts.tpl +57 -0
- package/templates/presets/identity/context/test/unit/decline-invitation.handler.spec.ts.tpl +40 -0
- package/templates/presets/identity/context/test/unit/delegation-granted.publisher.spec.ts.tpl +36 -0
- package/templates/presets/identity/context/test/unit/delegation-period.vo.spec.ts.tpl +32 -0
- package/templates/presets/identity/context/test/unit/delegation-revoked.publisher.spec.ts.tpl +35 -0
- package/templates/presets/identity/context/test/unit/delegation.aggregate.spec.ts.tpl +111 -0
- package/templates/presets/identity/context/test/unit/display-name.vo.spec.ts.tpl +13 -0
- package/templates/presets/identity/context/test/unit/email-address.vo.spec.ts.tpl +24 -0
- package/templates/presets/identity/context/test/unit/environment-identity-provider-connection.adapter.spec.ts.tpl +41 -0
- package/templates/presets/identity/context/test/unit/grant-delegation.controller.spec.ts.tpl +57 -0
- package/templates/presets/identity/context/test/unit/grant-delegation.handler.spec.ts.tpl +66 -0
- package/templates/presets/identity/context/test/unit/identity-application.ts.tpl +79 -0
- package/templates/presets/identity/context/test/unit/invitation.aggregate.spec.ts.tpl +118 -0
- package/templates/presets/identity/context/test/unit/invite-member.controller.spec.ts.tpl +57 -0
- package/templates/presets/identity/context/test/unit/invite-member.handler.spec.ts.tpl +110 -0
- package/templates/presets/identity/context/test/unit/member-joined.integration-handler.spec.ts.tpl +38 -0
- package/templates/presets/identity/context/test/unit/member-joined.publisher.spec.ts.tpl +36 -0
- package/templates/presets/identity/context/test/unit/member-removed.integration-handler.spec.ts.tpl +45 -0
- package/templates/presets/identity/context/test/unit/member-removed.publisher.spec.ts.tpl +35 -0
- package/templates/presets/identity/context/test/unit/member-role-changed.publisher.spec.ts.tpl +36 -0
- package/templates/presets/identity/context/test/unit/member.aggregate.spec.ts.tpl +142 -0
- package/templates/presets/identity/context/test/unit/open-organisation.controller.spec.ts.tpl +57 -0
- package/templates/presets/identity/context/test/unit/open-organisation.handler.spec.ts.tpl +52 -0
- package/templates/presets/identity/context/test/unit/organisation-name.vo.spec.ts.tpl +13 -0
- package/templates/presets/identity/context/test/unit/organisation-opened.integration-handler.spec.ts.tpl +24 -0
- package/templates/presets/identity/context/test/unit/organisation-opened.publisher.spec.ts.tpl +35 -0
- package/templates/presets/identity/context/test/unit/organisation-rights.vo.spec.ts.tpl +23 -0
- package/templates/presets/identity/context/test/unit/organisation-slug.vo.spec.ts.tpl +17 -0
- package/templates/presets/identity/context/test/unit/organisation.aggregate.spec.ts.tpl +33 -0
- package/templates/presets/identity/context/test/unit/provision-account.controller.spec.ts.tpl +73 -0
- package/templates/presets/identity/context/test/unit/provision-account.handler.spec.ts.tpl +65 -0
- package/templates/presets/identity/context/test/unit/remove-member.controller.spec.ts.tpl +44 -0
- package/templates/presets/identity/context/test/unit/remove-member.handler.spec.ts.tpl +125 -0
- package/templates/presets/identity/context/test/unit/revoke-delegation.controller.spec.ts.tpl +44 -0
- package/templates/presets/identity/context/test/unit/revoke-delegation.handler.spec.ts.tpl +51 -0
- package/templates/presets/identity/context/test/unit/revoke-invitation.controller.spec.ts.tpl +44 -0
- package/templates/presets/identity/context/test/unit/revoke-invitation.handler.spec.ts.tpl +44 -0
- package/templates/presets/identity/context/test/unit/role-rights.service.spec.ts.tpl +54 -0
- package/templates/presets/identity/context/test/unit/search-members.controller.spec.ts.tpl +43 -0
- package/templates/presets/identity/context/test/unit/search-members.handler.spec.ts.tpl +38 -0
- package/templates/presets/identity/decision.md.tpl +37 -0
- package/templates/presets/identity/design.md.tpl +71 -0
- package/templates/presets/signed-in-grants.ts.tpl +12 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ValueObject } from '@ddd-ts-kit/kernel';
|
|
2
|
+
import { AccountError } from '{{alias.context}}/domain/errors/account.error{{alias.ext}}';
|
|
3
|
+
|
|
4
|
+
const LONGEST = 100;
|
|
5
|
+
|
|
6
|
+
export type DisplayNameProps = {
|
|
7
|
+
readonly value: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** How a person is shown to the others, as they or their identity provider wrote it, spaces at the ends removed. */
|
|
11
|
+
export class DisplayName extends ValueObject<DisplayNameProps> {
|
|
12
|
+
private constructor(props: DisplayNameProps) {
|
|
13
|
+
super(props);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static create(raw: string): DisplayName {
|
|
17
|
+
const value = raw.trim();
|
|
18
|
+
|
|
19
|
+
if (value.length === 0 || value.length > LONGEST) {
|
|
20
|
+
throw AccountError.invalidDisplayName(raw);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return new DisplayName({ value });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get value(): string {
|
|
27
|
+
return this.props.value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ValueObject } from '@ddd-ts-kit/kernel';
|
|
2
|
+
import { AccountError } from '{{alias.context}}/domain/errors/account.error{{alias.ext}}';
|
|
3
|
+
|
|
4
|
+
/** The longest address the standards allow (RFC 5321, a path of 256 characters and its two brackets). */
|
|
5
|
+
const LONGEST = 254;
|
|
6
|
+
const SHAPE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
7
|
+
|
|
8
|
+
export type EmailAddressProps = {
|
|
9
|
+
readonly value: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Where a person is reached, compared without case: `Ada@Example.org` and `ada@example.org` are one address. */
|
|
13
|
+
export class EmailAddress extends ValueObject<EmailAddressProps> {
|
|
14
|
+
private constructor(props: EmailAddressProps) {
|
|
15
|
+
super(props);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static create(raw: string): EmailAddress {
|
|
19
|
+
const value = raw.trim().toLowerCase();
|
|
20
|
+
|
|
21
|
+
if (value.length > LONGEST || !SHAPE.test(value)) {
|
|
22
|
+
throw AccountError.invalidEmailAddress(raw);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return new EmailAddress({ value });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get value(): string {
|
|
29
|
+
return this.props.value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ValueObject } from '@ddd-ts-kit/kernel';
|
|
2
|
+
import { OrganisationError } from '{{alias.context}}/domain/errors/organisation.error{{alias.ext}}';
|
|
3
|
+
|
|
4
|
+
const LONGEST = 100;
|
|
5
|
+
|
|
6
|
+
export type OrganisationNameProps = {
|
|
7
|
+
readonly value: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** The name an organisation is shown under: free text, unlike its slug. */
|
|
11
|
+
export class OrganisationName extends ValueObject<OrganisationNameProps> {
|
|
12
|
+
private constructor(props: OrganisationNameProps) {
|
|
13
|
+
super(props);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static create(raw: string): OrganisationName {
|
|
17
|
+
const value = raw.trim();
|
|
18
|
+
|
|
19
|
+
if (value.length === 0 || value.length > LONGEST) {
|
|
20
|
+
throw OrganisationError.invalidOrganisationName(raw);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return new OrganisationName({ value });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get value(): string {
|
|
27
|
+
return this.props.value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ValueObject } from '@ddd-ts-kit/kernel';
|
|
2
|
+
import type { MemberRight } from '{{alias.context}}/domain/vocabularies/member-right.vocabulary{{alias.ext}}';
|
|
3
|
+
|
|
4
|
+
export type OrganisationRightsProps = {
|
|
5
|
+
readonly organisationId: string;
|
|
6
|
+
readonly accountId: string;
|
|
7
|
+
readonly rights: readonly MemberRight[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What one account may do in one organisation at one instant: the rights of its role, and those
|
|
12
|
+
* an active delegation lends it. The policies of the context decide on it; the platform authorizer
|
|
13
|
+
* is asked about the resource it names, the context, as for every use case of the context.
|
|
14
|
+
*/
|
|
15
|
+
export class OrganisationRights extends ValueObject<OrganisationRightsProps> {
|
|
16
|
+
readonly type = 'context';
|
|
17
|
+
|
|
18
|
+
readonly id = '{{context.kebab}}';
|
|
19
|
+
|
|
20
|
+
private constructor(props: OrganisationRightsProps) {
|
|
21
|
+
super(props);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static of(organisationId: string, accountId: string, rights: readonly MemberRight[]): OrganisationRights {
|
|
25
|
+
return new OrganisationRights({ organisationId, accountId, rights: [...new Set(rights)].sort() });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
includes(right: MemberRight): boolean {
|
|
29
|
+
return this.props.rights.includes(right);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get organisationId(): string {
|
|
33
|
+
return this.props.organisationId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get accountId(): string {
|
|
37
|
+
return this.props.accountId;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get rights(): readonly MemberRight[] {
|
|
41
|
+
return this.props.rights;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ValueObject } from '@ddd-ts-kit/kernel';
|
|
2
|
+
import { OrganisationError } from '{{alias.context}}/domain/errors/organisation.error{{alias.ext}}';
|
|
3
|
+
|
|
4
|
+
/** Lower-case letters, digits and hyphens, at most 63: a slug is also a valid tenant id and a valid alias of the identity provider. */
|
|
5
|
+
const SHAPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
6
|
+
|
|
7
|
+
export type OrganisationSlugProps = {
|
|
8
|
+
readonly value: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** The identifier of an organisation, chosen when it opens and never changed: in addresses, in tokens, as its tenant. */
|
|
12
|
+
export class OrganisationSlug extends ValueObject<OrganisationSlugProps> {
|
|
13
|
+
private constructor(props: OrganisationSlugProps) {
|
|
14
|
+
super(props);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static create(raw: string): OrganisationSlug {
|
|
18
|
+
const value = raw.trim().toLowerCase();
|
|
19
|
+
|
|
20
|
+
if (!SHAPE.test(value)) {
|
|
21
|
+
throw OrganisationError.invalidOrganisationSlug(raw);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return new OrganisationSlug({ value });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get value(): string {
|
|
28
|
+
return this.props.value;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/templates/presets/identity/context/domain/vocabularies/account-status.vocabulary.ts.tpl
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Whether an account may still act: a deactivated account keeps its history and signs in no more. */
|
|
2
|
+
export const AccountStatus = {
|
|
3
|
+
Active: 'active',
|
|
4
|
+
Deactivated: 'deactivated',
|
|
5
|
+
} as const;
|
|
6
|
+
|
|
7
|
+
export type AccountStatus = (typeof AccountStatus)[keyof typeof AccountStatus];
|
package/templates/presets/identity/context/domain/vocabularies/invitation-status.vocabulary.ts.tpl
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Where an invitation stands: pending until the person it is addressed to answers, or the organisation takes it back. */
|
|
2
|
+
export const InvitationStatus = {
|
|
3
|
+
Pending: 'pending',
|
|
4
|
+
Accepted: 'accepted',
|
|
5
|
+
Declined: 'declined',
|
|
6
|
+
Revoked: 'revoked',
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
export type InvitationStatus = (typeof InvitationStatus)[keyof typeof InvitationStatus];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** What a member may do in an organisation, granted by a role or lent by a delegation. */
|
|
2
|
+
export const MemberRight = {
|
|
3
|
+
ReadMembers: 'read-members',
|
|
4
|
+
InviteMembers: 'invite-members',
|
|
5
|
+
ManageMembers: 'manage-members',
|
|
6
|
+
GrantDelegations: 'grant-delegations',
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
export type MemberRight = (typeof MemberRight)[keyof typeof MemberRight];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The role of a member in an organisation: what the member may do there follows from it (`RoleRightsService`). */
|
|
2
|
+
export const MemberRole = {
|
|
3
|
+
Owner: 'owner',
|
|
4
|
+
Admin: 'admin',
|
|
5
|
+
Member: 'member',
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
export type MemberRole = (typeof MemberRole)[keyof typeof MemberRole];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { Adapter } from '@ddd-ts-kit/kernel/declarations';
|
|
3
|
+
import { InvitationTokens } from '{{alias.context}}/application/ports/invitation-tokens.port{{alias.ext}}';
|
|
4
|
+
|
|
5
|
+
/** 256 bits: a token is never guessed, and never stored. */
|
|
6
|
+
const TOKEN_BYTES = 32;
|
|
7
|
+
|
|
8
|
+
/** Tokens drawn from the random source of the platform, kept as their SHA-256 digest. */
|
|
9
|
+
@Adapter(InvitationTokens, { kind: 'external' })
|
|
10
|
+
export class CryptoInvitationTokens implements InvitationTokens {
|
|
11
|
+
issue(): { token: string; digest: string } {
|
|
12
|
+
const token = randomBytes(TOKEN_BYTES).toString('base64url');
|
|
13
|
+
|
|
14
|
+
return { token, digest: this.digestOf(token) };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
digestOf(token: string): string {
|
|
18
|
+
return createHash('sha256').update(token).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Adapter } from '@ddd-ts-kit/kernel/declarations';
|
|
2
|
+
import { IdentityProviderConnection } from '{{alias.context}}/application/ports/identity-provider-connection.port{{alias.ext}}';
|
|
3
|
+
|
|
4
|
+
/** The variables read, and what each one holds. */
|
|
5
|
+
const VARIABLES = {
|
|
6
|
+
baseUrl: 'KEYCLOAK_URL',
|
|
7
|
+
realm: 'KEYCLOAK_REALM',
|
|
8
|
+
clientId: 'IDENTITY_PROVIDER_ADMIN_CLIENT_ID',
|
|
9
|
+
clientSecret: 'IDENTITY_PROVIDER_ADMIN_CLIENT_SECRET',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The connection to the administration API of the provider, read from the environment when a call
|
|
14
|
+
* is made: an application that never calls it starts without it. A missing variable fails the
|
|
15
|
+
* call that needed it, with its name; its value is never written.
|
|
16
|
+
*/
|
|
17
|
+
@Adapter(IdentityProviderConnection, { kind: 'external' })
|
|
18
|
+
export class EnvironmentIdentityProviderConnection implements IdentityProviderConnection {
|
|
19
|
+
async settings(): Promise<{ baseUrl: string; realm: string; clientId: string; clientSecret: string }> {
|
|
20
|
+
return {
|
|
21
|
+
baseUrl: variable(VARIABLES.baseUrl).replace(/\/+$/, ''),
|
|
22
|
+
realm: variable(VARIABLES.realm),
|
|
23
|
+
clientId: variable(VARIABLES.clientId),
|
|
24
|
+
clientSecret: variable(VARIABLES.clientSecret),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function variable(name: string): string {
|
|
30
|
+
const value = process.env[name] ?? '';
|
|
31
|
+
|
|
32
|
+
if (value.trim() === '') {
|
|
33
|
+
throw new Error(`${name} is not set: the administration API of the identity provider is out of reach.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Adapter } from '@ddd-ts-kit/kernel/declarations';
|
|
2
|
+
import { IdentityProviderAdmin } from '{{alias.context}}/application/ports/identity-provider-admin.port{{alias.ext}}';
|
|
3
|
+
import type { IdentityProviderConnection } from '{{alias.context}}/application/ports/identity-provider-connection.port{{alias.ext}}';
|
|
4
|
+
|
|
5
|
+
const CONFLICT = 409;
|
|
6
|
+
const NOT_FOUND = 404;
|
|
7
|
+
|
|
8
|
+
/** How many organizations one read of the realm lists. */
|
|
9
|
+
const PAGE = 100;
|
|
10
|
+
|
|
11
|
+
interface Call {
|
|
12
|
+
readonly method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly body?: unknown;
|
|
15
|
+
/** Statuses that mean the change is already there: a retry finds it done. */
|
|
16
|
+
readonly done?: readonly number[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface KeycloakOrganisation {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly alias: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One page of the organizations of the realm: the one under the alias if it is there, and whether more may follow. */
|
|
25
|
+
interface SearchedPage {
|
|
26
|
+
readonly found: KeycloakOrganisation | undefined;
|
|
27
|
+
readonly full: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A recipe for the administration REST API of Keycloak (26 and later, which has organizations):
|
|
32
|
+
* the client of `IdentityProviderConnection` takes a token by the client credentials grant, then
|
|
33
|
+
* asks. An organisation is known there under its slug as alias, so that the `organization` claim
|
|
34
|
+
* of a token names it. Every change is idempotent, since the consumers that call it are delivered
|
|
35
|
+
* at least once (`application/integration/handlers/`). Its spec runs it against a local server
|
|
36
|
+
* that answers as Keycloak does, not against Keycloak itself: run it once against your realm
|
|
37
|
+
* before relying on it (docs/guides/bootstrap-with-well-designed-contexts.md says how).
|
|
38
|
+
*/
|
|
39
|
+
@Adapter(IdentityProviderAdmin, { kind: 'external' })
|
|
40
|
+
export class KeycloakIdentityProviderAdmin implements IdentityProviderAdmin {
|
|
41
|
+
constructor(private readonly connection: IdentityProviderConnection) {}
|
|
42
|
+
|
|
43
|
+
async openOrganisation(organisation: { readonly id: string; readonly name: string }): Promise<void> {
|
|
44
|
+
const body = { name: organisation.name, alias: organisation.id, enabled: true };
|
|
45
|
+
|
|
46
|
+
await this.send({ method: 'POST', path: 'organizations', body, done: [CONFLICT] });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async addMember(organisationId: string, accountId: string): Promise<void> {
|
|
50
|
+
const keycloakId = await this.organisationNamed(organisationId);
|
|
51
|
+
|
|
52
|
+
await this.send({ method: 'POST', path: `organizations/${keycloakId}/members`, body: accountId, done: [CONFLICT] });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async removeMember(organisationId: string, accountId: string): Promise<void> {
|
|
56
|
+
const keycloakId = await this.organisationNamed(organisationId);
|
|
57
|
+
const path = `organizations/${keycloakId}/members/${encodeURIComponent(accountId)}`;
|
|
58
|
+
|
|
59
|
+
await this.send({ method: 'DELETE', path, done: [NOT_FOUND] });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async disableAccount(accountId: string): Promise<void> {
|
|
63
|
+
await this.send({ method: 'PUT', path: `users/${encodeURIComponent(accountId)}`, body: { enabled: false } });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Keycloak searches organizations by name, not by alias: its pages are read until the alias is found. */
|
|
67
|
+
private async organisationNamed(alias: string): Promise<string> {
|
|
68
|
+
let first = 0;
|
|
69
|
+
let page = await this.searchPage(alias, first);
|
|
70
|
+
|
|
71
|
+
while (page.found === undefined && page.full) {
|
|
72
|
+
first += PAGE;
|
|
73
|
+
page = await this.searchPage(alias, first);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (page.found === undefined) {
|
|
77
|
+
throw new Error(`Keycloak knows no organization under the alias ${alias}.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return page.found.id;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private async searchPage(alias: string, first: number): Promise<SearchedPage> {
|
|
84
|
+
const answer = await this.send({ method: 'GET', path: `organizations?briefRepresentation=true&first=${first}&max=${PAGE}` });
|
|
85
|
+
const listed: unknown = await answer.json();
|
|
86
|
+
|
|
87
|
+
if (!Array.isArray(listed)) {
|
|
88
|
+
return { found: undefined, full: false };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { found: listed.find((candidate) => isOrganisation(candidate, alias)), full: listed.length === PAGE };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async send(call: Call): Promise<Response> {
|
|
95
|
+
const { baseUrl, realm } = await this.connection.settings();
|
|
96
|
+
const headers = { authorization: `Bearer ${await this.token()}`, 'content-type': 'application/json' };
|
|
97
|
+
const body = call.body === undefined ? {} : { body: JSON.stringify(call.body) };
|
|
98
|
+
|
|
99
|
+
const answer = await fetch(`${baseUrl}/admin/realms/${realm}/${call.path}`, {
|
|
100
|
+
method: call.method,
|
|
101
|
+
headers,
|
|
102
|
+
...body,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!answer.ok && !(call.done ?? []).includes(answer.status)) {
|
|
106
|
+
throw new Error(`Keycloak answered ${answer.status} to ${call.method} ${call.path.split('?')[0] ?? ''}.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return answer;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private async token(): Promise<string> {
|
|
113
|
+
const { baseUrl, realm, clientId, clientSecret } = await this.connection.settings();
|
|
114
|
+
const grant = { grant_type: 'client_credentials', client_id: clientId, client_secret: clientSecret };
|
|
115
|
+
|
|
116
|
+
const answer = await fetch(`${baseUrl}/realms/${realm}/protocol/openid-connect/token`, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
body: new URLSearchParams(grant),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const granted: unknown = await answer.json();
|
|
122
|
+
|
|
123
|
+
if (!answer.ok || !isToken(granted)) {
|
|
124
|
+
throw new Error(`Keycloak refused a token to the client ${clientId} (${answer.status}).`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return granted.access_token;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isOrganisation(candidate: unknown, alias: string): candidate is KeycloakOrganisation {
|
|
132
|
+
return (
|
|
133
|
+
typeof candidate === 'object' &&
|
|
134
|
+
candidate !== null &&
|
|
135
|
+
'id' in candidate &&
|
|
136
|
+
typeof candidate.id === 'string' &&
|
|
137
|
+
'alias' in candidate &&
|
|
138
|
+
candidate.alias === alias
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isToken(granted: unknown): granted is { readonly access_token: string } {
|
|
143
|
+
return (
|
|
144
|
+
typeof granted === 'object' &&
|
|
145
|
+
granted !== null &&
|
|
146
|
+
'access_token' in granted &&
|
|
147
|
+
typeof granted.access_token === 'string'
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// ddd-ts-kit:imports
|
|
2
|
+
import { sql } from 'drizzle-orm';
|
|
3
|
+
import { index, integer, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
|
4
|
+
import { auditColumns, contextSchema } from '@ddd-ts-kit/drizzle/postgres';
|
|
5
|
+
import type { AccountStatus } from '{{alias.context}}/domain/vocabularies/account-status.vocabulary{{alias.ext}}';
|
|
6
|
+
import type { InvitationStatus } from '{{alias.context}}/domain/vocabularies/invitation-status.vocabulary{{alias.ext}}';
|
|
7
|
+
import type { MemberRole } from '{{alias.context}}/domain/vocabularies/member-role.vocabulary{{alias.ext}}';
|
|
8
|
+
// /ddd-ts-kit:imports
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The tables of {{context.kebab}}: what the provider authenticates stays with it, no password is stored.
|
|
12
|
+
{{#if tenancy.any}}
|
|
13
|
+
* They hold no tenant column: the organisations are the tenants, and this context decides who
|
|
14
|
+
* belongs to which ({{preset.adr}}).
|
|
15
|
+
{{/if}}
|
|
16
|
+
*/
|
|
17
|
+
export const {{context.camel}}Schema = contextSchema('{{context.kebab}}');
|
|
18
|
+
|
|
19
|
+
const instant = (name: string) => timestamp(name, { withTimezone: true, mode: 'date' });
|
|
20
|
+
|
|
21
|
+
// ddd-ts-kit:tables
|
|
22
|
+
/** Keyed by the subject the identity provider gives the person. */
|
|
23
|
+
export const accounts = {{context.camel}}Schema.table('accounts', {
|
|
24
|
+
id: text('id').primaryKey(),
|
|
25
|
+
email: text('email').notNull(),
|
|
26
|
+
displayName: text('display_name').notNull(),
|
|
27
|
+
status: text('status').$type<AccountStatus>().notNull(),
|
|
28
|
+
provisionedAt: instant('provisioned_at').notNull(),
|
|
29
|
+
deactivatedAt: instant('deactivated_at'),
|
|
30
|
+
version: integer('version').notNull(),
|
|
31
|
+
...auditColumns(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** Keyed by the slug, which is also the tenant id when the project has tenants. */
|
|
35
|
+
export const organisations = {{context.camel}}Schema.table('organisations', {
|
|
36
|
+
id: text('id').primaryKey(),
|
|
37
|
+
name: text('name').notNull(),
|
|
38
|
+
openedBy: text('opened_by').notNull(),
|
|
39
|
+
openedAt: instant('opened_at').notNull(),
|
|
40
|
+
version: integer('version').notNull(),
|
|
41
|
+
...auditColumns(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** One current membership per account and organisation: the partial unique index is the guard. */
|
|
45
|
+
export const members = {{context.camel}}Schema.table(
|
|
46
|
+
'members',
|
|
47
|
+
{
|
|
48
|
+
id: text('id').primaryKey(),
|
|
49
|
+
organisationId: text('organisation_id').notNull(),
|
|
50
|
+
accountId: text('account_id').notNull(),
|
|
51
|
+
role: text('role').$type<MemberRole>().notNull(),
|
|
52
|
+
joinedAt: instant('joined_at').notNull(),
|
|
53
|
+
removedAt: instant('removed_at'),
|
|
54
|
+
version: integer('version').notNull(),
|
|
55
|
+
...auditColumns(),
|
|
56
|
+
},
|
|
57
|
+
(table) => [
|
|
58
|
+
uniqueIndex('members_current_idx').on(table.organisationId, table.accountId).where(sql`removed_at is null`),
|
|
59
|
+
index('members_account_idx').on(table.accountId),
|
|
60
|
+
],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
export const invitations = {{context.camel}}Schema.table(
|
|
64
|
+
'invitations',
|
|
65
|
+
{
|
|
66
|
+
id: text('id').primaryKey(),
|
|
67
|
+
organisationId: text('organisation_id').notNull(),
|
|
68
|
+
email: text('email').notNull(),
|
|
69
|
+
role: text('role').$type<MemberRole>().notNull(),
|
|
70
|
+
tokenHash: text('token_hash').notNull().unique(),
|
|
71
|
+
invitedBy: text('invited_by').notNull(),
|
|
72
|
+
issuedAt: instant('issued_at').notNull(),
|
|
73
|
+
expiresAt: instant('expires_at').notNull(),
|
|
74
|
+
status: text('status').$type<InvitationStatus>().notNull(),
|
|
75
|
+
answeredAt: instant('answered_at'),
|
|
76
|
+
version: integer('version').notNull(),
|
|
77
|
+
...auditColumns(),
|
|
78
|
+
},
|
|
79
|
+
(table) => [index('invitations_organisation_idx').on(table.organisationId, table.status)],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
export const delegations = {{context.camel}}Schema.table(
|
|
83
|
+
'delegations',
|
|
84
|
+
{
|
|
85
|
+
id: text('id').primaryKey(),
|
|
86
|
+
organisationId: text('organisation_id').notNull(),
|
|
87
|
+
delegatorId: text('delegator_id').notNull(),
|
|
88
|
+
delegateId: text('delegate_id').notNull(),
|
|
89
|
+
/** The rights lent, separated by commas: a right is a word of `MemberRight`, which holds none. */
|
|
90
|
+
rights: text('rights').notNull(),
|
|
91
|
+
startsAt: instant('starts_at').notNull(),
|
|
92
|
+
endsAt: instant('ends_at').notNull(),
|
|
93
|
+
endedAt: instant('ended_at'),
|
|
94
|
+
version: integer('version').notNull(),
|
|
95
|
+
...auditColumns(),
|
|
96
|
+
},
|
|
97
|
+
(table) => [
|
|
98
|
+
index('delegations_delegate_idx').on(table.organisationId, table.delegateId),
|
|
99
|
+
index('delegations_delegator_idx').on(table.organisationId, table.delegatorId),
|
|
100
|
+
],
|
|
101
|
+
);
|
|
102
|
+
// /ddd-ts-kit:tables
|
package/templates/presets/identity/context/infrastructure/persistence/account-row.schema.knex.ts.tpl
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AuditRow } from '@ddd-ts-kit/knex';
|
|
2
|
+
import type { AccountStatus } from '{{alias.context}}/domain/vocabularies/account-status.vocabulary{{alias.ext}}';
|
|
3
|
+
|
|
4
|
+
export interface AccountColumns {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly email: string;
|
|
7
|
+
readonly display_name: string;
|
|
8
|
+
readonly status: AccountStatus;
|
|
9
|
+
readonly provisioned_at: Date;
|
|
10
|
+
readonly deactivated_at: Date | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AccountRow extends AccountColumns, AuditRow {
|
|
14
|
+
readonly version: number;
|
|
15
|
+
}
|
package/templates/presets/identity/context/infrastructure/persistence/account.mapper.drizzle.ts.tpl
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AuditStamp } from '@ddd-ts-kit/kernel';
|
|
2
|
+
import { Account } from '{{alias.context}}/domain/aggregates/account.aggregate{{alias.ext}}';
|
|
3
|
+
import { DisplayName } from '{{alias.context}}/domain/value-objects/display-name.vo{{alias.ext}}';
|
|
4
|
+
import { EmailAddress } from '{{alias.context}}/domain/value-objects/email-address.vo{{alias.ext}}';
|
|
5
|
+
import type { accounts } from '{{alias.context}}/infrastructure/persistence/{{context.kebab}}.schema{{alias.ext}}';
|
|
6
|
+
|
|
7
|
+
type AccountRow = typeof accounts.$inferSelect;
|
|
8
|
+
|
|
9
|
+
export type AccountColumns = Omit<AccountRow, 'version' | keyof AuditStamp>;
|
|
10
|
+
|
|
11
|
+
export const AccountMapper = {
|
|
12
|
+
toDomain(row: AccountRow): Account {
|
|
13
|
+
const loaded = Account.rehydrate(row.id, {
|
|
14
|
+
email: EmailAddress.create(row.email),
|
|
15
|
+
displayName: DisplayName.create(row.displayName),
|
|
16
|
+
status: row.status,
|
|
17
|
+
provisionedAt: row.provisionedAt,
|
|
18
|
+
deactivatedAt: row.deactivatedAt ?? undefined,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
loaded.version = row.version;
|
|
22
|
+
|
|
23
|
+
return loaded;
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
toRow(saved: Account): AccountColumns {
|
|
27
|
+
const state = saved.toState();
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
id: saved.id,
|
|
31
|
+
email: state.email.value,
|
|
32
|
+
displayName: state.displayName.value,
|
|
33
|
+
status: state.status,
|
|
34
|
+
provisionedAt: state.provisionedAt,
|
|
35
|
+
deactivatedAt: state.deactivatedAt ?? null,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|
package/templates/presets/identity/context/infrastructure/persistence/account.mapper.knex.ts.tpl
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Account } from '{{alias.context}}/domain/aggregates/account.aggregate{{alias.ext}}';
|
|
2
|
+
import { DisplayName } from '{{alias.context}}/domain/value-objects/display-name.vo{{alias.ext}}';
|
|
3
|
+
import { EmailAddress } from '{{alias.context}}/domain/value-objects/email-address.vo{{alias.ext}}';
|
|
4
|
+
import type { AccountColumns, AccountRow } from '{{alias.context}}/infrastructure/persistence/account-row.schema{{alias.ext}}';
|
|
5
|
+
|
|
6
|
+
export const AccountMapper = {
|
|
7
|
+
toDomain(row: AccountRow): Account {
|
|
8
|
+
const loaded = Account.rehydrate(row.id, {
|
|
9
|
+
email: EmailAddress.create(row.email),
|
|
10
|
+
displayName: DisplayName.create(row.display_name),
|
|
11
|
+
status: row.status,
|
|
12
|
+
provisionedAt: row.provisioned_at,
|
|
13
|
+
deactivatedAt: row.deactivated_at ?? undefined,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
loaded.version = row.version;
|
|
17
|
+
|
|
18
|
+
return loaded;
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
toRow(saved: Account): AccountColumns {
|
|
22
|
+
const state = saved.toState();
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
id: saved.id,
|
|
26
|
+
email: state.email.value,
|
|
27
|
+
display_name: state.displayName.value,
|
|
28
|
+
status: state.status,
|
|
29
|
+
provisioned_at: state.provisionedAt,
|
|
30
|
+
deactivated_at: state.deactivatedAt ?? null,
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
};
|