From c22d54bcaffbd359af647e0b8867b91862e139c6 Mon Sep 17 00:00:00 2001 From: Cory Alder Date: Sat, 28 Feb 2026 11:32:14 -0800 Subject: [PATCH 01/10] squash the rebased code into a single commit --- .circleci/config.yml | 67 - .dockerignore | 21 +- .editorconfig | 16 +- .github/workflows/build-docker.yml | 4 +- .github/workflows/run-tests.yml | 50 + .gitignore | 263 +- .husky/pre-commit | 22 +- .husky/pre-push | 5 + .prettierignore | 58 +- Dockerfile | 34 - README.md | 88 +- Vagrantfile | 76 - app/constants/DateTime.php | 8 - app/constants/Errors.php | 7 - app/domain/AppClient.php | 30 - app/domain/EmailTemplate.php | 53 - app/domain/Event.php | 41 - app/domain/GenuineCard.php | 33 - app/domain/Ipn.php | 23 - app/domain/Key.php | 92 - app/domain/PasswordResetRequest.php | 28 - app/domain/Payment.php | 32 - app/domain/StripeEvent.php | 21 - app/domain/WebHook.php | 40 - app/exceptions/InvalidInputException.php | 9 - .../InvalidPasswordHashException.php | 9 - app/exceptions/MemberCardException.php | 9 - app/exceptions/UserAlreadyExistsException.php | 9 - app/monitors/DomainEventMonitor.php | 120 - app/schema/AccessTokenSchema.php | 38 - app/schema/EventPrivilegeSchema.php | 35 - app/schema/KeyPrivilegeSchema.php | 35 - app/schema/MembershipPrivilegeSchema.php | 35 - app/schema/RefreshTokenSchema.php | 38 - .../SystemPreferencePrivilegeSchema.php | 35 - app/schema/UserPrivilegeSchema.php | 35 - app/schema/WebHookPrivilegeSchema.php | 35 - .../slack/SlackProviderException.php | 12 - circle.yml | 9 - composer.json | 15 - conf/config.ini.php.template | 55 - conf/mimes.types | 89 - conf/nginx-vhost-docker-compose.conf | 30 - conf/nginx-vhost-vagrant.conf | 34 - conf/nginx-vhost-windows.conf | 56 - conf/nginx.conf | 67 +- docker-compose.dev.conf | 160 - docker-compose.sample.conf | 153 - docker-compose.sh | 20 - docker-compose.template.conf | 157 - docker-compose.yml | 98 + docker-compose/.dockerignore | 5 + docker-compose/Dockerfile | 120 +- docker-compose/build-backend.yml | 6 - docker-compose/build-frontend.yml | 6 - docker-compose/core.network-bridge.yml | 6 - .../core.network-proxy-internal.yml | 15 - docker-compose/core.network-proxy.yml | 12 - docker-compose/core.ports.yml | 4 - docker-compose/core.yml | 14 - docker-compose/files-local-backend.yml | 7 - docker-compose/files-local-frontend.yml | 5 - docker-compose/files-local-nomos-env.yml | 4 - docker-compose/mysql-external-mysqld.yml | 4 - .../mysql-external-network-mysql-lithium.yml | 9 - .../mysql-external-network-mysql.yml | 8 - docker-compose/mysql-external-variable.yml | 4 - docker-compose/mysql-local-network-bridge.yml | 3 - docker-compose/mysql-local-network-mysql.yml | 11 - docker-compose/mysql-local.yml | 22 - docker-compose/nginx-proxy-devtest.yml | 5 - docker-compose/nginx-proxy-prod.yml | 5 - {docker => docker-compose}/nomos.env.template | 10 +- docker-compose/override.yml | 13 - docker-compose/rabbitmq-external.yml | 16 - docker-compose/rabbitmq-local-management.yml | 4 - .../rabbitmq-local-network-bridge.yml | 6 - .../rabbitmq-local-network-internal.yml | 15 - .../rabbitmq-local-network-rabbitmq.yml | 16 - docker-compose/rabbitmq-local.yml | 21 - docker-compose/traefik-devtest.yml | 10 - docker-compose/traefik-prod.yml | 10 - docker-compose/webhooker-build.yml | 7 - docker-compose/webhooker-logs-local.yml | 4 - docker-compose/webhooker-network-bridge.yml | 3 - docker-compose/webhooker-network-proxy.yml | 8 - docker-compose/webhooker-network-rabbitmq.yml | 12 - docker-compose/webhooker.yml | 9 - docker/docker_compose_run.sh | 7 - docker/docker_env_config.sh | 9 - eslint.config.mjs | 16 - justfile | 122 +- nomos.d.ts | 45 - package.json | 67 +- .../backend-php/.php-cs-fixer.php | 56 +- .../app/adapters/v2/EmailAdapter2.php | 70 + {app => packages/backend-php/app}/app.php | 8 +- packages/backend-php/app/constants/Errors.php | 11 + .../backend-php/app/constants/Formats.php | 9 + .../app}/constants/StringLiterals.php | 1 + .../app}/contracts/IApiKeyService1.php | 23 +- .../app}/contracts/IAuthService1.php | 123 +- .../app}/contracts/IEmailService1.php | 77 +- .../app}/contracts/IEventService1.php | 49 +- .../app}/contracts/IIpnService1.php | 15 +- .../app}/contracts/IKeyService1.php | 27 +- .../app}/contracts/IMemberCardService1.php | 56 +- .../app}/contracts/IMembershipService1.php | 61 +- .../app}/contracts/IMetricService1.php | 11 +- .../app}/contracts/IPaymentService1.php | 33 +- .../app}/contracts/IPinService1.php | 18 +- .../app}/contracts/IPreferenceService1.php | 45 +- .../app}/contracts/IPrivilegeService1.php | 45 +- .../app}/contracts/IStripeEventService1.php | 15 +- .../app}/contracts/IUserService1.php | 119 +- .../app}/contracts/IWebHookService1.php | 85 +- .../app/contracts/v2/IApiKeyService2.php | 90 + .../app/contracts/v2/IAuthService2.php | 164 + .../app/contracts/v2/IEmailService2.php | 173 + .../app/contracts/v2/IEventService2.php | 140 + .../app/contracts/v2/IIpnService2.php | 51 + .../app/contracts/v2/IKeyService2.php | 91 + .../app/contracts/v2/IMemberCardService2.php | 110 + .../app/contracts/v2/IMembershipService2.php | 130 + .../app/contracts/v2/IMetricService2.php | 99 + .../app/contracts/v2/IOAuthService2.php | 213 + .../app/contracts/v2/IPaymentService2.php | 80 + .../app/contracts/v2/IPinService2.php | 76 + .../app/contracts/v2/IPreferenceService2.php | 117 + .../app/contracts/v2/IPrivilegeService2.php | 132 + .../app/contracts/v2/IStripeEventService2.php | 52 + .../v2/ISystemPreferenceService2.php | 119 + .../app/contracts/v2/IUserService2.php | 279 + .../app/contracts/v2/IWebHookService2.php | 152 + .../backend-php/app}/domain/AccessLog.php | 43 +- .../backend-php/app}/domain/AccessToken.php | 33 +- packages/backend-php/app/domain/AppClient.php | 55 + .../backend-php/app/domain/EmailTemplate.php | 96 + packages/backend-php/app/domain/Event.php | 76 + .../backend-php/app/domain/GenuineCard.php | 63 + packages/backend-php/app/domain/Ipn.php | 52 + packages/backend-php/app/domain/Key.php | 254 + .../backend-php/app}/domain/Membership.php | 65 +- .../app/domain/PasswordResetRequest.php | 60 + packages/backend-php/app/domain/Payment.php | 79 + .../backend-php/app}/domain/Privilege.php | 63 +- .../backend-php/app}/domain/RefreshToken.php | 35 +- .../backend-php/app/domain/StripeEvent.php | 50 + .../app}/domain/SystemPreference.php | 33 +- .../backend-php/app}/domain/User.php | 115 +- packages/backend-php/app/domain/WebHook.php | 80 + packages/backend-php/app/dto/AccessLog.php | 33 + packages/backend-php/app/dto/AccessToken.php | 27 + packages/backend-php/app/dto/AppClient.php | 39 + .../backend-php/app/dto/AppClientInfo.php | 25 + .../backend-php/app/dto/EmailTemplate.php | 33 + packages/backend-php/app/dto/Event.php | 30 + .../app/dto/GeneratedEmailResults.php | 14 + packages/backend-php/app/dto/GenuineCard.php | 39 + packages/backend-php/app/dto/Ipn.php | 44 + .../backend-php/app/dto/IpnValidationEnum.php | 13 + packages/backend-php/app/dto/Key.php | 35 + packages/backend-php/app/dto/KeyTypeEnum.php | 18 + packages/backend-php/app/dto/Membership.php | 45 + .../app/dto/PasswordResetRequest.php | 24 + packages/backend-php/app/dto/Payment.php | 59 + .../backend-php/app/dto/PaymentPpEnum.php | 14 + packages/backend-php/app/dto/Privilege.php | 30 + packages/backend-php/app/dto/RefreshToken.php | 27 + .../backend-php/app/dto/SavedRefreshToken.php | 17 + .../backend-php/app/dto/ServiceResponse.php | 14 + .../app/dto/ServiceResponseError.php | 8 + .../dto/ServiceResponseErrorInvalidToken.php | 10 + ...esponseErrorUserNotFoundByEmailAddress.php | 10 + .../app/dto/ServiceResponseSuccess.php | 10 + packages/backend-php/app/dto/StripeEvent.php | 44 + .../app/dto/StripeEventStatusEnum.php | 13 + .../backend-php/app/dto/SystemPreference.php | 27 + .../app/dto/TotalKeyHoldersResult.php | 7 + .../app/dto/TotalMembersResult.php | 7 + .../backend-php/app/dto/TrimmedAppClient.php | 29 + .../app/dto/TrimmedAppClientOwner.php | 35 + packages/backend-php/app/dto/TrimmedUser.php | 35 + packages/backend-php/app/dto/User.php | 84 + .../backend-php/app/dto/UserActiveEnum.php | 15 + .../backend-php/app/dto/UserPrincipal.php | 24 + packages/backend-php/app/dto/WebHook.php | 42 + .../v2/MetricServiceGetCreatedDatesResult.php | 18 + .../dto/v2/MetricServiceGetMembersResult.php | 53 + .../dto/v2/MetricServiceGetRevenueResult.php | 18 + .../v2/MetricServiceNewKeyholdersResult.php | 15 + .../dto/v2/MetricServiceNewMembersResult.php | 19 + .../v2/MetricServiceTotalKeyHoldersResult.php | 15 + .../v2/MetricServiceTotalMembersResult.php | 15 + .../app/dto/v2/MetricServiceValueResult.php | 13 + .../app/endpoints/v2/ApiKeyService2.svc.php | 21 + .../app/endpoints/v2/AuthService2.svc.php | 21 + .../app/endpoints/v2/EmailService2.svc.php | 21 + .../app/endpoints/v2/EventService2.svc.php | 21 + .../app/endpoints/v2/IpnService2.svc.php | 14 + .../app/endpoints/v2/KeyService2.svc.php | 21 + .../endpoints/v2/MemberCardService2.svc.php | 21 + .../endpoints/v2/MembershipService2.svc.php | 14 + .../app/endpoints/v2/MetricService2.svc.php | 14 + .../app/endpoints/v2/OAuthService2.svc.php | 21 + .../app/endpoints/v2/PaymentService2.svc.php | 14 + .../app/endpoints/v2/PinService2.svc.php | 21 + .../endpoints/v2/PreferenceService2.svc.php | 21 + .../endpoints/v2/PrivilegeService2.svc.php | 21 + .../endpoints/v2/StripeEventService2.svc.php | 14 + .../v2/SystemPreferenceService2.svc.php | 21 + .../app/endpoints/v2/UserService2.svc.php | 21 + .../app/endpoints/v2/WebHookService2.svc.php | 21 + .../app}/endpoints/web/ApiKeyService1.svc.php | 1 + .../app}/endpoints/web/AuthService1.svc.php | 1 + .../app}/endpoints/web/EmailService1.svc.php | 1 + .../app}/endpoints/web/EventService1.svc.php | 1 + .../app}/endpoints/web/IpnService1.svc.php | 1 + .../app}/endpoints/web/KeyService1.svc.php | 1 + .../endpoints/web/MemberCardService1.svc.php | 1 + .../endpoints/web/MembershipService1.svc.php | 1 + .../app}/endpoints/web/MetricService1.svc.php | 1 + .../endpoints/web/PaymentService1.svc.php | 1 + .../app}/endpoints/web/PinService1.svc.php | 1 + .../endpoints/web/PreferenceService1.svc.php | 1 + .../endpoints/web/PrivilegeService1.svc.php | 1 + .../endpoints/web/StripeEventService1.svc.php | 1 + .../app}/endpoints/web/UserService1.svc.php | 1 + .../endpoints/web/WebHookService1.svc.php | 1 + .../app/enums/MetricServiceGroupType.php | 11 + ...InvalidAccessTokenCredentialsException.php | 12 + .../app/exceptions/InvalidInputException.php | 19 + .../InvalidKeyCredentialsException.php | 17 + .../InvalidPasswordHashException.php | 18 + .../app/exceptions/MemberCardException.php | 19 + .../exceptions/UserAlreadyExistsException.php | 18 + .../app}/gateways/IPaymentGateway.php | 13 + .../app}/gateways/PaymentGatewayException.php | 1 + .../app}/gateways/PaypalGateway.php | 48 +- .../app}/gateways/StripeGateway.php | 63 +- .../app/handlers/v2/ApiKeyServiceHandler2.php | 219 + .../app/handlers/v2/AuthServiceHandler2.php | 471 + .../app/handlers/v2/EmailServiceHandler2.php | 266 + .../app/handlers/v2/EventServiceHandler2.php | 276 + .../app/handlers/v2/IpnServiceHandler2.php | 67 + .../app/handlers/v2/KeyServiceHandler2.php | 241 + .../handlers/v2/MemberCardServiceHandler2.php | 289 + .../handlers/v2/MembershipServiceHandler2.php | 221 + .../app/handlers/v2/MetricServiceHandler2.php | 485 + .../app/handlers/v2/OAuthServiceHandler2.php | 485 + .../handlers/v2/PaymentServiceHandler2.php | 154 + .../app/handlers/v2/PinServiceHandler2.php | 227 + .../handlers/v2/PreferenceServiceHandler2.php | 239 + .../handlers/v2/PrivilegeServiceHandler2.php | 252 + .../v2/StripeEventServiceHandler2.php | 72 + .../v2/SystemPreferenceServiceHandler2.php | 260 + .../app/handlers/v2/UserServiceHandler2.php | 803 + .../handlers/v2/WebHookServiceHandler2.php | 303 + {app => packages/backend-php/app}/include.php | 22 +- .../modules/HttpPaymentGatewayHandler.php | 10 +- .../HttpPaymentGatewayHandlerModule.php | 16 +- .../app}/monitors/PaymentMonitor.php | 20 +- .../app}/monitors/PaypalIpnMonitor.php | 17 +- .../app}/monitors/StripeEventMonitor.php | 23 +- .../app}/processors/PaymentProcessor.php | 113 +- .../backend-php/app}/resources/Resource.php | 9 + .../app}/schema/AccessLogSchema.php | 5 + .../app/schema/AccessTokenSchema.php | 63 + .../app}/schema/AppClientSchema.php | 17 +- .../backend-php/app}/schema/EmailSchema.php | 3 + .../app/schema/EventPrivilegeSchema.php | 60 + .../backend-php/app}/schema/EventSchema.php | 3 + .../app}/schema/GenuineCardSchema.php | 5 + .../backend-php/app}/schema/IpnSchema.php | 3 + .../app/schema/KeyPrivilegeSchema.php | 62 + .../backend-php/app}/schema/KeySchema.php | 17 +- .../app/schema/MembershipPrivilegeSchema.php | 60 + .../app}/schema/MembershipSchema.php | 5 + .../schema/PasswordResetRequestSchema.php | 17 +- .../backend-php/app}/schema/PaymentSchema.php | 31 +- .../app}/schema/PrivilegeSchema.php | 3 + .../app/schema/RefreshTokenSchema.php | 63 + .../app}/schema/SettingsSchema.php | 1 + .../app}/schema/StripeEventSchema.php | 3 + .../SystemPreferencePrivilegeSchema.php | 60 + .../app}/schema/SystemPreferenceSchema.php | 3 + .../app/schema/UserPrivilegeSchema.php | 60 + .../backend-php/app}/schema/UserSchema.php | 23 +- .../app/schema/WebHookPrivilegeSchema.php | 60 + .../backend-php/app}/schema/WebHookSchema.php | 29 +- .../app}/security/Authenticate.php | 167 +- .../app}/security/ColumnPrivilegedAccess.php | 63 +- .../app}/security/HttpApiAuthModule.php | 31 + .../app}/security/PasswordUtil.php | 34 +- .../app}/security/PrivilegedAccess.php | 118 +- .../app}/security/TablePrivilegedAccess.php | 56 +- .../app}/security/TokenPrincipal.php | 74 + .../app}/security/UserPrincipal.php | 82 + .../security/credentials/ApiCredentials.php | 1 + .../security/credentials/PinCredentials.php | 1 + .../security/credentials/RfidCredentials.php | 1 + .../security/credentials/TokenCredentials.php | 20 +- .../app}/security/oauth/OAuthHelper.php | 52 + .../oauth/modules/GithubOAuthHandler.php | 17 +- .../oauth/modules/GoogleOAuthHandler.php | 15 +- .../security/oauth/modules/OAuthHandler.php | 6 + .../oauth/modules/OAuthHandlerModule.php | 15 +- .../oauth/modules/SlackOAuthHandler.php | 15 +- .../security/oauth/providers/slack/Slack.php | 73 +- .../slack/SlackProviderException.php | 23 + .../oauth/providers/slack/SlackUser.php | 30 +- .../app}/services/ApiKeyService.php | 57 +- .../backend-php/app}/services/AuthService.php | 306 +- .../app}/services/EmailService.php | 122 +- .../app}/services/EventService.php | 65 +- .../backend-php/app}/services/IpnService.php | 14 +- .../backend-php/app}/services/KeyService.php | 57 +- .../app}/services/MemberCardService.php | 106 +- .../app}/services/MembershipService.php | 79 +- .../app}/services/MetricService.php | 102 +- .../app}/services/PaymentService.php | 52 +- .../backend-php/app}/services/PinService.php | 65 +- .../app}/services/PreferenceService.php | 58 +- .../app}/services/PrivilegeService.php | 60 +- .../app}/services/StripeEventService.php | 15 +- .../backend-php/app}/services/UserService.php | 176 +- .../app}/services/WebHookService.php | 113 +- .../backend-php/app/utils/AuthCheckResult.php | 36 + packages/backend-php/app/utils/DTO.php | 36 + packages/backend-php/app/utils/EnumMapper.php | 23 + packages/backend-php/app/utils/IDTO.php | 6 + .../app/utils/converters/PHP2TS.php | 233 + packages/backend-php/composer.json | 24 + .../backend-php/composer.lock | 3757 ++++- .../backend-php/conf}/config.docker.ini.php | 8 +- .../backend-php/conf/config.ini.template.php | 48 + .../backend-php/conf/nginx.conf | 15 +- .../backend-php/conf/php-fpm/00-defaults.conf | 11 + .../conf/php-fpm/99-no-daemonize.conf | 2 + .../conf/php/99-no-fastcgi-logging.ini | 1 + packages/backend-php/conf/php/99-opcache.ini | 2 + .../backend-php/conf/php/99-sessions-dir.ini | 1 + .../backend-php/docker/docker_compose_run.sh | 20 + .../backend-php/docker/docker_env_config.sh | 12 + .../docker/docker_env_config_local.sh | 10 + .../backend-php/docker}/docker_run.sh | 2 +- packages/backend-php/justfile | 73 + packages/backend-php/package.json | 44 + packages/backend-php/php-ts-transformer.php | 34 + packages/backend-php/phpstan.neon | 23 + .../backend-php/phpunit.xml | 0 packages/backend-php/psalm.xml | 19 + packages/backend-php/tests/ConstantsTest.php | 92 + .../backend-php/tests}/DomainTest.php | 134 +- .../tests}/EmailTemplateDomainTest.php | 55 +- .../backend-php/tests}/KeyDomainTest.php | 50 +- .../backend-php/tests}/LimitTest.php | 59 +- .../backend-php/tests}/ServiceTest.php | 221 +- .../backend-php/tests}/WhereTest.php | 124 +- .../backend-php/tests}/autoload.php | 6 +- .../tests}/contracts/ITestService1.php | 13 +- .../backend-php/tests}/domain/Enchantment.php | 18 +- .../tests/domain/ExampleDomain.php | 84 + .../backend-php/tests}/domain/Knight.php | 21 +- .../backend-php/tests}/domain/Ring.php | 21 +- .../backend-php/tests}/domain/Sword.php | 19 +- .../endpoints/native/TestService1.svc.php | 1 + .../tests}/endpoints/web/TestService1.svc.php | 1 + .../tests}/schema/EnchantmentSchema.php | 3 + .../tests/schema/ExampleSchema.php | 33 + .../tests}/schema/KnightSchema.php | 19 +- .../backend-php/tests/schema/RingSchema.php | 58 + .../tests/schema/SwordEnchantmentsSchema.php | 61 + .../backend-php/tests}/schema/SwordSchema.php | 3 + .../tests/security/PermPrincipal.php | 86 + .../tests}/services/TestService.php | 14 +- .../backend-php/tools}/composer.sh | 0 .../tools/generate-dto-domain-classes.php | 148 + .../tools/generate-dto-schema-classes.php | 72 + .../generate-ts-contract-implementation.php | 57 + .../tools/generate-ts-contract-interface.php | 53 + .../backend-php/tools}/migrate.php | 6 +- .../backend-php/tools/sync-dto-classes.sh | 17 + packages/backend-php/vhs/BasePath.php | 15 + .../backend-php/vhs}/Cloneable.php | 18 +- packages/backend-php/vhs/Logger.php | 47 + packages/backend-php/vhs/Loggington.php | 19 + .../backend-php/vhs}/Singleton.php | 18 +- .../backend-php/vhs}/SplClassLoader.php | 72 +- .../backend-php/vhs}/database/Column.php | 79 +- .../backend-php/vhs}/database/Columns.php | 58 + .../vhs}/database/ConnectionInfo.php | 11 + .../backend-php/vhs}/database/Database.php | 145 +- .../backend-php/vhs}/database/Element.php | 8 + .../backend-php/vhs}/database/Engine.php | 23 + .../vhs}/database/IColumnGenerator.php | 8 + .../backend-php/vhs}/database/IConverter.php | 1 + .../vhs}/database/IConvertible.php | 1 + .../vhs/database/IDataInterface.php | 114 + .../vhs}/database/IGeneratable.php | 5 +- .../backend-php/vhs}/database/IGenerator.php | 1 + .../vhs}/database/IOnGenerator.php | 8 + .../vhs/database/ITableGenerator.php | 22 + .../backend-php/vhs}/database/On.php | 36 +- .../backend-php/vhs}/database/Table.php | 65 +- .../vhs/database/access/IAccess.php | 38 + .../vhs}/database/access/IAccessGenerator.php | 1 + .../vhs}/database/constraints/Constraint.php | 46 +- .../vhs}/database/constraints/ForeignKey.php | 8 +- .../constraints/IConstraintGenerator.php | 15 + .../vhs}/database/constraints/PrimaryKey.php | 1 + .../engines/memory/InMemoryEngine.php | 129 +- .../engines/memory/InMemoryGenerator.php | 241 +- .../engines/mysql/MySqlConnectionInfo.php | 61 +- .../database/engines/mysql/MySqlConverter.php | 67 +- .../database/engines/mysql/MySqlEngine.php | 147 +- .../database/engines/mysql/MySqlGenerator.php | 233 +- .../DatabaseConnectionException.php | 1 + .../database/exceptions/DatabaseException.php | 1 + .../exceptions/InvalidSchemaException.php | 1 + .../vhs/database/joins/IJoinGenerator.php | 60 + .../backend-php/vhs/database/joins/Join.php | 149 + .../vhs}/database/joins/JoinCross.php | 1 + .../vhs}/database/joins/JoinInner.php | 1 + .../vhs}/database/joins/JoinLeft.php | 1 + .../vhs}/database/joins/JoinOuter.php | 1 + .../vhs}/database/joins/JoinRight.php | 1 + .../vhs}/database/limits/ILimitGenerator.php | 8 + .../vhs}/database/limits/Limit.php | 30 +- .../database/offsets/IOffsetGenerator.php | 8 + .../vhs/database/offsets/Offset.php | 61 + .../database/orders/IOrderByGenerator.php | 15 + .../vhs}/database/orders/OrderBy.php | 46 +- .../vhs}/database/orders/OrderByAscending.php | 8 + .../database/orders/OrderByDescending.php | 8 + .../vhs/database/queries/IQueryGenerator.php | 60 + .../vhs/database/queries/Query.php | 159 + .../vhs}/database/queries/QueryCount.php | 22 +- .../vhs}/database/queries/QueryDelete.php | 11 +- .../vhs}/database/queries/QueryInsert.php | 13 +- .../vhs}/database/queries/QuerySelect.php | 31 +- .../vhs}/database/queries/QueryUpdate.php | 21 +- .../vhs/database/types/ITypeConverter.php | 95 + .../vhs/database/types/ITypeGenerator.php | 95 + .../backend-php/vhs}/database/types/Type.php | 110 +- .../vhs}/database/types/TypeBool.php | 3 +- .../vhs}/database/types/TypeDate.php | 3 +- .../vhs}/database/types/TypeDateTime.php | 3 +- .../vhs}/database/types/TypeEnum.php | 19 +- .../vhs}/database/types/TypeFloat.php | 3 +- .../vhs}/database/types/TypeInt.php | 3 +- .../vhs}/database/types/TypeString.php | 11 +- .../vhs}/database/types/TypeText.php | 3 +- .../vhs/database/wheres/IWhereGenerator.php | 42 + .../vhs}/database/wheres/Where.php | 113 +- .../vhs}/database/wheres/WhereAnd.php | 31 +- .../vhs}/database/wheres/WhereComparator.php | 81 +- .../vhs}/database/wheres/WhereOr.php | 8 + .../backend-php/vhs}/domain/Domain.php | 566 +- .../vhs}/domain/DomainValueCache.php | 51 + packages/backend-php/vhs/domain/Filter.php | 198 + packages/backend-php/vhs/domain/IDomain.php | 13 + packages/backend-php/vhs/domain/ISchema.php | 13 + .../backend-php/vhs}/domain/Notifier.php | 58 + .../backend-php/vhs}/domain/Schema.php | 61 +- .../collections/ChildDomainCollection.php | 102 +- .../domain/collections/DomainCollection.php | 243 + .../collections/ParentDomainCollection.php | 65 + .../collections/SatelliteDomainCollection.php | 98 +- .../domain/exceptions/DomainException.php | 1 + .../InvalidColumnDefinitionException.php | 1 + .../validations/ValidationException.php | 15 + .../domain/validations/ValidationFailure.php | 18 + .../domain/validations/ValidationResults.php | 18 +- .../vhs/exceptions/HttpException.php | 25 + packages/backend-php/vhs/gateways/Engine.php | 175 + .../vhs/gateways/interfaces/IGateway.php | 7 + .../interfaces/IMessagesEmailGateway.php | 28 + .../gateways/messages/email/AWSSESClient.php | 115 + .../vhs}/loggers/ConsoleLogger.php | 8 + .../backend-php/vhs}/loggers/FileLogger.php | 35 + .../backend-php/vhs}/loggers/SilentLogger.php | 8 + .../backend-php/vhs}/loggers/StringLogger.php | 16 +- .../backend-php/vhs}/migration/Backup.php | 97 +- .../backend-php/vhs}/migration/Migrator.php | 79 +- packages/backend-php/vhs/monitors/Monitor.php | 29 + .../vhs}/security/AnonPrincipal.php | 1 + .../vhs}/security/BearerTokenCredentials.php | 18 + .../backend-php/vhs}/security/CurrentUser.php | 70 +- .../vhs}/security/IAuthenticate.php | 15 +- .../vhs}/security/ICredentials.php | 1 + .../backend-php/vhs/security/IPrincipal.php | 74 + .../vhs}/security/SystemPrincipal.php | 3 +- .../vhs}/security/UserPassCredentials.php | 30 + .../exceptions/InvalidCredentials.php | 20 + .../exceptions/UnauthorizedException.php | 20 + .../backend-php/vhs}/services/IContract.php | 1 + packages/backend-php/vhs/services/Service.php | 43 + .../vhs}/services/ServiceClient.php | 11 +- .../vhs}/services/ServiceContext.php | 21 +- .../vhs}/services/ServiceHandler.php | 68 +- .../vhs}/services/ServiceRegistry.php | 16 +- .../vhs}/services/endpoints/Endpoint.php | 66 +- .../vhs/services/endpoints/IEndpoint.php | 49 + .../vhs}/services/endpoints/JsonEndpoint.php | 29 + .../services/endpoints/NativeEndpoint.php | 29 + .../exceptions/InvalidContractException.php | 1 + .../exceptions/InvalidRequestException.php | 33 + {vhs => packages/backend-php/vhs}/vhs.php | 0 .../backend-php/vhs}/web/HttpContext.php | 15 +- .../backend-php/vhs}/web/HttpRequest.php | 8 +- .../vhs}/web/HttpRequestHandler.php | 8 + .../backend-php/vhs}/web/HttpServer.php | 162 +- .../backend-php/vhs}/web/HttpUtil.php | 20 +- packages/backend-php/vhs/web/IHttpModule.php | 41 + .../vhs/web/enums/HttpStatusCodes.php | 71 + .../vhs}/web/modules/HttpBasicAuthModule.php | 51 +- .../web/modules/HttpBearerTokenAuthModule.php | 43 +- .../modules/HttpExceptionHandlerModule.php | 49 +- .../modules/HttpJsonServiceHandlerModule.php | 122 + .../web/modules/HttpRequestHandlerModule.php | 42 + .../vhs}/web/modules/HttpServerInfoModule.php | 35 + packages/frontend-react/.bowerrc | 5 + packages/frontend-react/.editorconfig | 9 + packages/frontend-react/.prettierignore | 5 + packages/frontend-react/.storybook/main.ts | 52 + .../.storybook/preview-head.html | 4 + .../frontend-react/.storybook/preview.tsx | 40 + packages/frontend-react/README.md | 20 + packages/frontend-react/bower.json | 14 + .../conf/nginx-react-docker-compose.conf | 31 + packages/frontend-react/docs/Development.md | 84 + packages/frontend-react/eslint.config.mjs | 100 + .../frontend-react/generate-react-cli.json | 280 + packages/frontend-react/index.html | 20 + packages/frontend-react/package.json | 216 + packages/frontend-react/postcss.config.js | 7 + packages/frontend-react/prettier.config.mjs | 5 + .../public/apiMockServiceWorker.js | 12 + .../public}/badges/cert_cnc_mill_lathe.svg | 0 .../public}/badges/cert_laser.svg | 0 .../public}/badges/door_access.svg | 0 .../public}/badges/key_holder.svg | 0 .../public}/badges/key_holder_inactive.svg | 0 .../frontend-react/public}/badges/laser.png | Bin .../public}/badges/newsletter.svg | 0 .../public/custom/pace.local.css | 22 + .../frontend-react/public}/favicon.ico | Bin .../frontend-react/public}/images/logo.png | Bin .../frontend-react/public/images/logo.svg | 43 + .../public}/images/provider/github.png | Bin .../public}/images/provider/google.png | Bin .../public}/images/provider/pin.png | Bin .../public}/images/provider/rfid.png | Bin .../public}/images/provider/slack.png | Bin .../frontend-react/public}/images/slack.png | Bin .../images/under-construction90s-90s.gif | Bin 0 -> 3541 bytes .../images/vhs-member-card-2015-2-full.png | Bin .../images/vhs-member-card-2015-2-thumb.png | Bin .../images/vhs-member-card-2015-full.png | Bin .../images/vhs-member-card-2015-thumb.png | Bin .../public/mockServiceWorker.js | 302 + packages/frontend-react/public/robots.txt | 3 + packages/frontend-react/skel/config.json | 3 + .../src/components/01-atoms/Col/Col.lazy.tsx | 15 + .../components/01-atoms/Col/Col.stories.tsx | 29 + .../src/components/01-atoms/Col/Col.tsx | 34 + .../src/components/01-atoms/Col/Col.types.ts | 11 + .../src/components/01-atoms/Col/Col.utils.ts | 0 .../Conditional/Conditional.stories.tsx | 18 + .../01-atoms/Conditional/Conditional.tsx | 12 + .../01-atoms/Conditional/Conditional.types.ts | 7 + .../01-atoms/Conditional/Conditional.utils.ts | 0 .../01-atoms/Container/Container.lazy.tsx | 15 + .../01-atoms/Container/Container.stories.tsx | 27 + .../01-atoms/Container/Container.tsx | 17 + .../01-atoms/Container/Container.types.ts | 7 + .../01-atoms/Container/Container.utils.ts | 0 .../FontAwesomeIcon/FontAwesomeIcon.lazy.tsx | 15 + .../FontAwesomeIcon.stories.tsx | 24 + .../FontAwesomeIcon/FontAwesomeIcon.tsx | 47 + .../FontAwesomeIcon/FontAwesomeIcon.types.ts | 27 + .../FontAwesomeIcon/FontAwesomeIcon.utils.ts | 0 .../01-atoms/NavBar/NavBar.lazy.tsx | 15 + .../01-atoms/NavBar/NavBar.stories.tsx | 29 + .../src/components/01-atoms/NavBar/NavBar.tsx | 14 + .../01-atoms/NavBar/NavBar.types.ts | 5 + .../01-atoms/NavBar/NavBar.utils.ts | 0 .../01-atoms/Overlay/Overlay.lazy.tsx | 15 + .../01-atoms/Overlay/Overlay.stories.tsx | 29 + .../components/01-atoms/Overlay/Overlay.tsx | 31 + .../01-atoms/Overlay/Overlay.types.ts | 8 + .../01-atoms/Overlay/Overlay.utils.ts | 0 .../components/01-atoms/Pill/Pill.lazy.tsx | 15 + .../components/01-atoms/Pill/Pill.stories.tsx | 21 + .../src/components/01-atoms/Pill/Pill.tsx | 19 + .../components/01-atoms/Pill/Pill.types.ts | 6 + .../components/01-atoms/Pill/Pill.utils.ts | 0 .../01-atoms/Popover/Popover.lazy.tsx | 15 + .../01-atoms/Popover/Popover.stories.tsx | 28 + .../components/01-atoms/Popover/Popover.tsx | 20 + .../01-atoms/Popover/Popover.types.ts | 7 + .../01-atoms/Popover/Popover.utils.ts | 0 .../src/components/01-atoms/Row/Row.lazy.tsx | 15 + .../components/01-atoms/Row/Row.stories.tsx | 29 + .../src/components/01-atoms/Row/Row.tsx | 21 + .../src/components/01-atoms/Row/Row.types.ts | 8 + .../src/components/01-atoms/Row/Row.utils.ts | 0 .../TableActionsCell.lazy.tsx | 15 + .../TableActionsCell.stories.tsx | 27 + .../TableActionsCell/TableActionsCell.tsx | 11 + .../TableActionsCell.types.ts | 6 + .../TableActionsCell.utils.ts | 0 .../TablePageRow/TablePageRow.lazy.tsx | 15 + .../TablePageRow/TablePageRow.settings.ts | 17 + .../TablePageRow/TablePageRow.stories.tsx | 30 + .../01-atoms/TablePageRow/TablePageRow.tsx | 21 + .../TablePageRow/TablePageRow.types.ts | 5 + .../TablePageRow/TablePageRow.utils.ts | 0 .../AccountStatusBadge.lazy.tsx | 15 + .../AccountStatusBadge.module.css | 11 + .../AccountStatusBadge.stories.tsx | 80 + .../AccountStatusBadge/AccountStatusBadge.tsx | 45 + .../AccountStatusBadge.types.ts | 5 + .../AccountStatusBadge.utils.ts | 0 .../AdminGuard/AdminGuard.lazy.tsx | 15 + .../AdminGuard/AdminGuard.stories.tsx | 29 + .../02-molecules/AdminGuard/AdminGuard.tsx | 23 + .../AdminGuard/AdminGuard.types.ts | 5 + .../AdminGuard/AdminGuard.utils.ts | 0 .../02-molecules/Button/Button.lazy.tsx | 15 + .../02-molecules/Button/Button.stories.tsx | 86 + .../02-molecules/Button/Button.styles.ts | 18 + .../components/02-molecules/Button/Button.tsx | 31 + .../02-molecules/Button/Button.types.ts | 22 + .../02-molecules/Button/Button.utils.ts | 0 .../ComplexChart/ComplexChart.lazy.tsx | 15 + .../ComplexChart/ComplexChart.stories.tsx | 34 + .../ComplexChart/ComplexChart.tsx | 24 + .../ComplexChart/ComplexChart.types.ts | 7 + .../ComplexChart/ComplexChart.utils.ts | 0 .../DoughnutChart/DoughnutChart.lazy.tsx | 15 + .../DoughnutChart/DoughnutChart.stories.tsx | 23 + .../DoughnutChart/DoughnutChart.tsx | 13 + .../DoughnutChart/DoughnutChart.types.ts | 6 + .../DoughnutChart/DoughnutChart.utils.ts | 0 .../EnabledCheckMark.lazy.tsx | 15 + .../EnabledCheckMark.stories.tsx | 39 + .../EnabledCheckMark.test.tsx | 10 + .../EnabledCheckMark/EnabledCheckMark.tsx | 36 + .../EnabledCheckMark.types.ts | 10 + .../EnabledCheckMark.utils.ts | 0 .../02-molecules/FormCol/FormCol.lazy.tsx | 15 + .../02-molecules/FormCol/FormCol.stories.tsx | 27 + .../02-molecules/FormCol/FormCol.test.tsx | 10 + .../02-molecules/FormCol/FormCol.tsx | 21 + .../02-molecules/FormCol/FormCol.types.ts | 8 + .../02-molecules/FormCol/FormCol.utils.ts | 0 .../02-molecules/FormRow/FormRow.lazy.tsx | 15 + .../02-molecules/FormRow/FormRow.stories.tsx | 27 + .../02-molecules/FormRow/FormRow.test.tsx | 10 + .../02-molecules/FormRow/FormRow.tsx | 21 + .../02-molecules/FormRow/FormRow.types.ts | 8 + .../02-molecules/FormRow/FormRow.utils.ts | 0 .../InputGroup/InputGroup.lazy.tsx | 15 + .../InputGroup/InputGroup.stories.tsx | 29 + .../02-molecules/InputGroup/InputGroup.tsx | 22 + .../InputGroup/InputGroup.types.ts | 7 + .../InputGroup/InputGroup.utils.ts | 0 .../LinkButton/LinkButton.lazy.tsx | 15 + .../LinkButton/LinkButton.stories.tsx | 27 + .../LinkButton/LinkButton.test.tsx | 10 + .../02-molecules/LinkButton/LinkButton.tsx | 33 + .../LinkButton/LinkButton.types.ts | 9 + .../LinkButton/LinkButton.utils.ts | 0 .../02-molecules/Loading/Loading.stories.tsx | 23 + .../02-molecules/Loading/Loading.tsx | 21 + .../02-molecules/Loading/Loading.types.ts | 3 + .../02-molecules/Loading/Loading.utils.ts | 7 + .../NotFoundComponent.lazy.tsx | 15 + .../NotFoundComponent.stories.tsx | 27 + .../NotFoundComponent.test.tsx | 10 + .../NotFoundComponent/NotFoundComponent.tsx | 14 + .../NotFoundComponent.types.ts | 1 + .../NotFoundComponent.utils.ts | 0 .../PrivilegeIcon/PrivilegeIcon.lazy.tsx | 15 + .../PrivilegeIcon/PrivilegeIcon.stories.tsx | 21 + .../PrivilegeIcon/PrivilegeIcon.test.tsx | 10 + .../PrivilegeIcon/PrivilegeIcon.tsx | 26 + .../PrivilegeIcon/PrivilegeIcon.types.ts | 11 + .../PrivilegeIcon/PrivilegeIcon.ui.tsx | 17 + .../PrivilegeIcon/PrivilegeIcon.utils.ts | 0 .../RedirectAdminDashboard.lazy.tsx | 15 + .../RedirectAdminDashboard.stories.tsx | 29 + .../RedirectAdminDashboard.tsx | 9 + .../RedirectAdminDashboard.types.ts | 5 + .../RedirectAdminDashboard.utils.ts | 0 .../RedirectLogin/RedirectLogin.lazy.tsx | 15 + .../RedirectLogin/RedirectLogin.stories.tsx | 29 + .../RedirectLogin/RedirectLogin.tsx | 9 + .../RedirectLogin/RedirectLogin.types.ts | 5 + .../RedirectLogin/RedirectLogin.utils.ts | 0 .../RedirectRoot/RedirectRoot.lazy.tsx | 15 + .../RedirectRoot/RedirectRoot.stories.tsx | 29 + .../RedirectRoot/RedirectRoot.tsx | 9 + .../RedirectRoot/RedirectRoot.types.ts | 5 + .../RedirectRoot/RedirectRoot.utils.ts | 0 .../RedirectUserDashboard.lazy.tsx | 15 + .../RedirectUserDashboard.stories.tsx | 29 + .../RedirectUserDashboard.tsx | 9 + .../RedirectUserDashboard.types.ts | 5 + .../RedirectUserDashboard.utils.ts | 0 .../SpaciousRow/SpaciousRow.lazy.tsx | 15 + .../SpaciousRow/SpaciousRow.stories.tsx | 27 + .../SpaciousRow/SpaciousRow.test.tsx | 10 + .../02-molecules/SpaciousRow/SpaciousRow.tsx | 15 + .../SpaciousRow/SpaciousRow.types.ts | 7 + .../SpaciousRow/SpaciousRow.utils.ts | 0 .../TableDataCell/TableDataCell.lazy.tsx | 15 + .../TableDataCell/TableDataCell.stories.tsx | 27 + .../TableDataCell/TableDataCell.test.tsx | 10 + .../TableDataCell/TableDataCell.tsx | 26 + .../TableDataCell/TableDataCell.types.ts | 5 + .../TableDataCell/TableDataCell.utils.ts | 0 .../02-molecules/Toggle/Toggle.lazy.tsx | 15 + .../02-molecules/Toggle/Toggle.stories.tsx | 49 + .../components/02-molecules/Toggle/Toggle.tsx | 61 + .../02-molecules/Toggle/Toggle.types.ts | 9 + .../02-molecules/Toggle/Toggle.utils.ts | 0 .../UnderConstructionBanner.lazy.tsx | 15 + .../UnderConstructionBanner.stories.tsx | 27 + .../UnderConstructionBanner.tsx | 31 + .../UnderConstructionBanner.types.ts | 5 + .../UnderConstructionBanner.utils.ts | 0 .../02-molecules/UserGuard/UserGuard.lazy.tsx | 15 + .../UserGuard/UserGuard.stories.tsx | 29 + .../02-molecules/UserGuard/UserGuard.tsx | 23 + .../02-molecules/UserGuard/UserGuard.types.ts | 5 + .../02-molecules/UserGuard/UserGuard.utils.ts | 0 .../UserProfileCard/UserProfileCard.lazy.tsx | 15 + .../UserProfileCard.stories.tsx | 29 + .../UserProfileCard/UserProfileCard.tsx | 38 + .../UserProfileCard/UserProfileCard.types.ts | 5 + .../UserProfileCard/UserProfileCard.utils.ts | 0 .../03-particles/Card/Card.stories.tsx | 15 + .../src/components/03-particles/Card/Card.tsx | 19 + .../03-particles/Card/Card.types.ts | 13 + .../Card/CardBody/CardBody.lazy.tsx | 15 + .../Card/CardBody/CardBody.stories.tsx | 29 + .../03-particles/Card/CardBody/CardBody.tsx | 13 + .../Card/CardBody/CardBody.types.ts | 6 + .../Card/CardBody/CardBody.utils.ts | 0 .../Card/CardContainer/CardContainer.lazy.tsx | 15 + .../CardContainer/CardContainer.stories.tsx | 29 + .../Card/CardContainer/CardContainer.tsx | 21 + .../Card/CardContainer/CardContainer.types.ts | 10 + .../Card/CardContainer/CardContainer.utils.ts | 0 .../Card/CardFooter/CardFooter.lazy.tsx | 15 + .../Card/CardFooter/CardFooter.stories.tsx | 29 + .../Card/CardFooter/CardFooter.tsx | 25 + .../Card/CardFooter/CardFooter.types.ts | 7 + .../Card/CardFooter/CardFooter.utils.ts | 0 .../Card/CardHeader/CardHeader.lazy.tsx | 15 + .../Card/CardHeader/CardHeader.stories.tsx | 29 + .../Card/CardHeader/CardHeader.tsx | 13 + .../Card/CardHeader/CardHeader.types.ts | 6 + .../Card/CardHeader/CardHeader.utils.ts | 0 .../CreateUserModal/CreateUserModal.lazy.tsx | 15 + .../CreateUserModal.stories.tsx | 27 + .../CreateUserModal/CreateUserModal.test.tsx | 10 + .../CreateUserModal/CreateUserModal.tsx | 15 + .../CreateUserModal/CreateUserModal.types.ts | 5 + .../CreateUserModal/CreateUserModal.utils.ts | 0 .../FormControl/FormControl.lazy.tsx | 15 + .../FormControl/FormControl.module.css | 52 + .../FormControl/FormControl.stories.tsx | 380 + .../03-particles/FormControl/FormControl.tsx | 46 + .../FormControl/FormControl.types.ts | 137 + .../FormControl/FormControl.utils.ts | 0 .../FormControlContainer.lazy.tsx | 15 + .../FormControlContainer.module.css | 1 + .../FormControlContainer.stories.tsx | 27 + .../FormControlContainer.tsx | 42 + .../FormControlContainer.types.ts | 11 + .../FormControlContainer.utils.ts | 0 .../FormControlDefault.lazy.tsx | 15 + .../FormControlDefault.module.css | 1 + .../FormControlDefault.stories.tsx | 27 + .../FormControlDefault/FormControlDefault.tsx | 91 + .../FormControlDefault.types.ts | 0 .../FormControlDefault.utils.ts | 0 .../FormControlDropdown.lazy.tsx | 15 + .../FormControlDropdown.module.css | 1 + .../FormControlDropdown.stories.tsx | 27 + .../FormControlDropdown.tsx | 65 + .../FormControlDropdown.types.ts | 0 .../FormControlDropdown.utils.ts | 0 .../FormControlPin/FormControlPin.lazy.tsx | 15 + .../FormControlPin/FormControlPin.module.css | 1 + .../FormControlPin/FormControlPin.stories.tsx | 27 + .../FormControlPin/FormControlPin.tsx | 78 + .../FormControlPin/FormControlPin.types.ts | 0 .../FormControlPin/FormControlPin.utils.ts | 13 + .../FormControlTextArea.lazy.tsx | 15 + .../FormControlTextArea.module.css | 1 + .../FormControlTextArea.stories.tsx | 27 + .../FormControlTextArea.tsx | 57 + .../FormControlTextArea.types.ts | 0 .../FormControlTextArea.utils.ts | 0 .../03-particles/GridChart/GridChart.lazy.tsx | 15 + .../GridChart/GridChart.module.css | 28 + .../GridChart/GridChart.stories.tsx | 64 + .../03-particles/GridChart/GridChart.tsx | 88 + .../03-particles/GridChart/GridChart.types.ts | 24 + .../03-particles/GridChart/GridChart.utils.ts | 24 + .../LoadingOverlay/LoadingOverlay.lazy.tsx | 13 + .../LoadingOverlay/LoadingOverlay.stories.tsx | 29 + .../LoadingOverlay/LoadingOverlay.tsx | 19 + .../LoadingOverlay/LoadingOverlay.types.ts | 3 + .../LoadingOverlay/LoadingOverlay.utils.ts | 0 .../03-particles/Menu/Menu.lazy.tsx | 15 + .../03-particles/Menu/Menu.stories.tsx | 28 + .../03-particles/Menu/Menu.test.tsx | 11 + .../src/components/03-particles/Menu/Menu.tsx | 29 + .../03-particles/Menu/Menu.types.ts | 3 + .../03-particles/Menu/Menu.utils.ts | 6 + .../03-particles/Menu/MenuItem/MenuItem.css | 29 + .../Menu/MenuItem/MenuItem.lazy.tsx | 15 + .../Menu/MenuItem/MenuItem.module.css | 3 + .../Menu/MenuItem/MenuItem.stories.tsx | 27 + .../Menu/MenuItem/MenuItem.test.tsx | 12 + .../03-particles/Menu/MenuItem/MenuItem.tsx | 44 + .../Menu/MenuItem/MenuItem.types.ts | 3 + .../Menu/MenuItem/MenuItem.utils.ts | 1 + .../03-particles/Modal/Modal.stories.tsx | 15 + .../03-particles/Modal/Modal.types.ts | 15 + .../Modal/ModalBody/ModalBody.lazy.tsx | 15 + .../Modal/ModalBody/ModalBody.stories.tsx | 29 + .../Modal/ModalBody/ModalBody.tsx | 13 + .../Modal/ModalBody/ModalBody.types.ts | 6 + .../Modal/ModalBody/ModalBody.utils.ts | 0 .../ModalContainer/ModalContainer.lazy.tsx | 15 + .../ModalContainer/ModalContainer.stories.tsx | 29 + .../Modal/ModalContainer/ModalContainer.tsx | 34 + .../ModalContainer/ModalContainer.types.ts | 7 + .../ModalContainer/ModalContainer.utils.ts | 0 .../Modal/ModalFooter/ModalFooter.lazy.tsx | 15 + .../Modal/ModalFooter/ModalFooter.stories.tsx | 29 + .../Modal/ModalFooter/ModalFooter.tsx | 16 + .../Modal/ModalFooter/ModalFooter.types.ts | 6 + .../Modal/ModalFooter/ModalFooter.utils.ts | 0 .../Modal/ModalHeader/ModalHeader.lazy.tsx | 15 + .../Modal/ModalHeader/ModalHeader.stories.tsx | 29 + .../Modal/ModalHeader/ModalHeader.tsx | 32 + .../Modal/ModalHeader/ModalHeader.types.ts | 7 + .../Modal/ModalHeader/ModalHeader.utils.ts | 0 .../Modal/ModalTitle/ModalTitle.lazy.tsx | 15 + .../Modal/ModalTitle/ModalTitle.stories.tsx | 29 + .../Modal/ModalTitle/ModalTitle.tsx | 13 + .../Modal/ModalTitle/ModalTitle.types.ts | 6 + .../Modal/ModalTitle/ModalTitle.utils.ts | 0 .../components/03-particles/Modal/index.tsx | 22 + .../03-particles/Paginator/Paginator.lazy.tsx | 15 + .../Paginator/Paginator.module.css | 23 + .../Paginator/Paginator.stories.tsx | 18 + .../03-particles/Paginator/Paginator.tsx | 186 + .../03-particles/Paginator/Paginator.types.ts | 41 + .../03-particles/Paginator/Paginator.utils.ts | 1 + .../PrivilegePill/PrivilegePill.lazy.tsx | 15 + .../PrivilegePill/PrivilegePill.stories.tsx | 32 + .../PrivilegePill/PrivilegePill.test.tsx | 19 + .../PrivilegePill/PrivilegePill.tsx | 26 + .../PrivilegePill/PrivilegePill.types.ts | 5 + .../PrivilegePill/PrivilegePill.utils.ts | 0 .../03-particles/TopBar/TopBar.lazy.tsx | 15 + .../03-particles/TopBar/TopBar.stories.tsx | 29 + .../components/03-particles/TopBar/TopBar.tsx | 35 + .../03-particles/TopBar/TopBar.types.ts | 5 + .../03-particles/TopBar/TopBar.utils.ts | 0 .../AdminStatusWidget.lazy.tsx | 15 + .../AdminStatusWidget.stories.tsx | 31 + .../AdminStatusWidget.test.tsx | 10 + .../AdminStatusWidget/AdminStatusWidget.tsx | 77 + .../AdminStatusWidget.types.ts | 11 + .../AdminStatusWidget.utils.ts | 0 .../04-composites/BasePage/BasePage.lazy.tsx | 15 + .../BasePage/BasePage.module.css | 3 + .../BasePage/BasePage.stories.tsx | 30 + .../04-composites/BasePage/BasePage.tsx | 84 + .../04-composites/BasePage/BasePage.types.ts | 8 + .../04-composites/BasePage/BasePage.utils.ts | 0 .../CurrentUserPrivilegesCard.lazy.tsx | 15 + .../CurrentUserPrivilegesCard.mocks.ts | 62 + .../CurrentUserPrivilegesCard.stories.tsx | 28 + .../CurrentUserPrivilegesCard.test.tsx | 11 + .../CurrentUserPrivilegesCard.tsx | 46 + .../CurrentUserPrivilegesCard.types.ts | 14 + .../CurrentUserPrivilegesCard.utils.ts | 0 .../CurrentUserPrivilegesList.lazy.tsx | 15 + .../CurrentUserPrivilegesList.stories.tsx | 27 + .../CurrentUserPrivilegesList.test.tsx | 21 + .../CurrentUserPrivilegesList.tsx | 29 + .../CurrentUserPrivilegesList.types.ts | 6 + .../CurrentUserPrivilegesList.utils.ts | 0 .../DomainSelectorCard.lazy.tsx | 15 + .../DomainSelectorCard.stories.tsx | 27 + .../DomainSelectorCard.test.tsx | 10 + .../DomainSelectorCard/DomainSelectorCard.tsx | 9 + .../DomainSelectorCard.types.ts | 5 + .../DomainSelectorCard.utils.ts | 0 .../MembershipSelectorCard.lazy.tsx | 15 + .../MembershipSelectorCard.stories.tsx | 27 + .../MembershipSelectorCard.test.tsx | 16 + .../MembershipSelectorCard.tsx | 58 + .../MembershipSelectorCard.types.ts | 8 + .../MembershipSelectorCard.utils.ts | 0 .../MobileMenu/MobileMenu.lazy.tsx | 15 + .../MobileMenu/MobileMenu.stories.tsx | 35 + .../MobileMenu/MobileMenu.test.tsx | 11 + .../04-composites/MobileMenu/MobileMenu.tsx | 55 + .../MobileMenu/MobileMenu.types.ts | 3 + .../MobileMenu/MobileMenu.utils.ts | 6 + .../OverlayCard/OverlayCard.lazy.tsx | 15 + .../OverlayCard/OverlayCard.stories.tsx | 27 + .../OverlayCard/OverlayCard.test.tsx | 10 + .../04-composites/OverlayCard/OverlayCard.tsx | 66 + .../OverlayCard/OverlayCard.types.ts | 17 + .../OverlayCard/OverlayCard.utils.ts | 0 .../PrivilegesSelectorCard.lazy.tsx | 15 + .../PrivilegesSelectorCard.stories.tsx | 42 + .../PrivilegesSelectorCard.test.tsx | 20 + .../PrivilegesSelectorCard.tsx | 89 + .../PrivilegesSelectorCard.types.ts | 15 + .../PrivilegesSelectorCard.utils.ts | 0 .../RootComponent/RootComponent.lazy.tsx | 15 + .../RootComponent/RootComponent.stories.tsx | 27 + .../RootComponent/RootComponent.test.tsx | 10 + .../RootComponent/RootComponent.tsx | 38 + .../RootComponent/RootComponent.types.ts | 5 + .../RootComponent/RootComponent.utils.ts | 0 .../SelectorCard/SelectorCard.lazy.tsx | 15 + .../SelectorCard/SelectorCard.stories.tsx | 27 + .../SelectorCard/SelectorCard.test.tsx | 10 + .../SelectorCard/SelectorCard.tsx | 77 + .../SelectorCard/SelectorCard.types.ts | 9 + .../SelectorCard/SelectorCard.utils.ts | 23 + .../04-composites/Tabs/Tab/Tab.lazy.tsx | 15 + .../04-composites/Tabs/Tab/Tab.stories.tsx | 27 + .../04-composites/Tabs/Tab/Tab.test.tsx | 14 + .../components/04-composites/Tabs/Tab/Tab.tsx | 11 + .../04-composites/Tabs/Tab/Tab.types.ts | 7 + .../04-composites/Tabs/Tab/Tab.utils.ts | 0 .../04-composites/Tabs/Tabs.lazy.tsx | 15 + .../04-composites/Tabs/Tabs.stories.tsx | 27 + .../04-composites/Tabs/Tabs.test.tsx | 10 + .../components/04-composites/Tabs/Tabs.tsx | 39 + .../04-composites/Tabs/Tabs.types.ts | 9 + .../04-composites/Tabs/Tabs.utils.ts | 0 .../WaitingRoom/WaitingRoom.lazy.tsx | 15 + .../WaitingRoom/WaitingRoom.stories.tsx | 29 + .../04-composites/WaitingRoom/WaitingRoom.tsx | 17 + .../WaitingRoom/WaitingRoom.types.ts | 5 + .../WaitingRoom/WaitingRoom.utils.ts | 0 .../InfoButton/InfoButton.lazy.tsx | 15 + .../InfoButton/InfoButton.stories.tsx | 22 + .../InfoButton/InfoButton.test.tsx | 10 + .../05-materials/InfoButton/InfoButton.tsx | 41 + .../InfoButton/InfoButton.types.ts | 8 + .../InfoButton/InfoButton.utils.ts | 0 .../ItemDeleteModal/ItemDeleteModal.lazy.tsx | 15 + .../ItemDeleteModal.stories.tsx | 27 + .../ItemDeleteModal/ItemDeleteModal.test.tsx | 22 + .../ItemDeleteModal/ItemDeleteModal.tsx | 28 + .../ItemDeleteModal/ItemDeleteModal.types.ts | 9 + .../ItemDeleteModal/ItemDeleteModal.utils.ts | 0 .../TablePage/TablePage.context.ts | 13 + .../05-materials/TablePage/TablePage.lazy.tsx | 15 + .../TablePage/TablePage.schema.ts | 58 + .../TablePage/TablePage.stories.tsx | 92 + .../05-materials/TablePage/TablePage.test.tsx | 22 + .../05-materials/TablePage/TablePage.tsx | 569 + .../05-materials/TablePage/TablePage.types.ts | 71 + .../05-materials/TablePage/TablePage.utils.ts | 35 + .../AdminLayout/AdminLayout.lazy.tsx | 15 + .../AdminLayout/AdminLayout.stories.tsx | 29 + .../06-layouts/AdminLayout/AdminLayout.tsx | 14 + .../AdminLayout/AdminLayout.types.ts | 5 + .../06-layouts/AdminLayout/AdminLayout.ui.tsx | 27 + .../AdminLayout/AdminLayout.utils.ts | 0 .../AuthenticatedLayout.lazy.tsx | 15 + .../AuthenticatedLayout.stories.tsx | 27 + .../AuthenticatedLayout.test.tsx | 16 + .../AuthenticatedLayout.tsx | 36 + .../AuthenticatedLayout.types.ts | 11 + .../AuthenticatedLayout.utils.ts | 0 .../06-layouts/MainLayout/MainLayout.lazy.tsx | 15 + .../MainLayout/MainLayout.stories.tsx | 29 + .../06-layouts/MainLayout/MainLayout.tsx | 17 + .../06-layouts/MainLayout/MainLayout.types.ts | 5 + .../06-layouts/MainLayout/MainLayout.utils.ts | 0 .../06-layouts/UserLayout/UserLayout.lazy.tsx | 15 + .../UserLayout/UserLayout.stories.tsx | 27 + .../06-layouts/UserLayout/UserLayout.tsx | 14 + .../06-layouts/UserLayout/UserLayout.types.ts | 5 + .../06-layouts/UserLayout/UserLayout.ui.tsx | 31 + .../06-layouts/UserLayout/UserLayout.utils.ts | 0 .../ApiKeysCreateNewButton.lazy.tsx | 15 + .../ApiKeysCreateNewButton.stories.tsx | 27 + .../ApiKeysCreateNewButton.tsx | 37 + .../ApiKeysCreateNewButton.types.ts | 14 + .../ApiKeysCreateNewButton.utils.ts | 0 .../ApiKeysEditModal.lazy.tsx | 15 + .../ApiKeysEditModal.stories.tsx | 42 + .../ApiKeysEditModal/ApiKeysEditModal.tsx | 218 + .../ApiKeysEditModal.types.ts | 8 + .../ApiKeysEditModal.utils.ts | 0 .../ApiKeysHelpPage/ApiKeysHelpPage.lazy.tsx | 15 + .../ApiKeysHelpPage.stories.tsx | 29 + .../ApiKeysHelpPage/ApiKeysHelpPage.tsx | 25 + .../ApiKeysHelpPage/ApiKeysHelpPage.types.ts | 5 + .../ApiKeysHelpPage/ApiKeysHelpPage.utils.ts | 0 .../ApiKeysListItem/ApiKeysListItem.lazy.tsx | 15 + .../ApiKeysListItem.stories.tsx | 42 + .../ApiKeysListItem/ApiKeysListItem.tsx | 101 + .../ApiKeysListItem/ApiKeysListItem.types.ts | 8 + .../ApiKeysListItem/ApiKeysListItem.utils.ts | 0 .../ApiKeysListPage/ApiKeysListPage.lazy.tsx | 15 + .../ApiKeysListPage.stories.tsx | 27 + .../ApiKeysListPage/ApiKeysListPage.tsx | 87 + .../ApiKeysListPage/ApiKeysListPage.types.ts | 5 + .../ApiKeysListPage/ApiKeysListPage.utils.ts | 0 .../ApiKeysNewModal/ApiKeysNewModal.lazy.tsx | 15 + .../ApiKeysNewModal.stories.tsx | 25 + .../ApiKeysNewModal/ApiKeysNewModal.tsx | 113 + .../ApiKeysNewModal/ApiKeysNewModal.types.ts | 1 + .../ApiKeysNewModal/ApiKeysNewModal.utils.ts | 0 .../ApiKeysPage/ApiKeysPage.context.ts | 15 + .../ApiKeysPage/ApiKeysPage.guard.ts | 33 + .../ApiKeysPage/ApiKeysPage.hooks.tsx | 73 + .../ApiKeysPage/ApiKeysPage.lazy.tsx | 15 + .../ApiKeysPage/ApiKeysPage.module.css | 11 + .../ApiKeysPage/ApiKeysPage.schemas.ts | 30 + .../ApiKeysPage/ApiKeysPage.settings.ts | 30 + .../ApiKeysPage/ApiKeysPage.stories.tsx | 41 + .../ApiKeysPage/ApiKeysPage.tsx | 85 + .../ApiKeysPage/ApiKeysPage.types.ts | 43 + .../ApiKeysPage/ApiKeysPage.utils.ts | 37 + .../ApiKeysPageContainer.lazy.tsx | 15 + .../ApiKeysPageContainer.stories.tsx | 27 + .../ApiKeysPageContainer.test.tsx | 24 + .../ApiKeysPageContainer.tsx | 74 + .../ApiKeysPageContainer.types.ts | 10 + .../ApiKeysPageContainer.utils.ts | 0 .../ApiKeysUsagePage.lazy.tsx | 15 + .../ApiKeysUsagePage.stories.tsx | 29 + .../ApiKeysUsagePage/ApiKeysUsagePage.tsx | 110 + .../ApiKeysUsagePage.types.ts | 5 + .../ApiKeysUsagePage.utils.ts | 0 .../OAuthPage/OAuthPage.context.ts | 15 + .../OAuthPage/OAuthPage.guards.ts | 24 + .../OAuthPage/OAuthPage.lazy.tsx | 15 + .../OAuthPage/OAuthPage.schemas.ts | 21 + .../OAuthPage/OAuthPage.settings.ts | 28 + .../OAuthPage/OAuthPage.stories.tsx | 27 + .../OAuthPage/OAuthPage.test.tsx | 10 + .../OAuthPage/OAuthPage.tsx | 58 + .../OAuthPage/OAuthPage.types.ts | 30 + .../OAuthPage/OAuthPage.utils.ts | 40 + .../OAuthPageClientItem.lazy.tsx | 15 + .../OAuthPageClientItem.stories.tsx | 25 + .../OAuthPageClientItem.test.tsx | 38 + .../OAuthPageClientItem.tsx | 114 + .../OAuthPageClientItem.types.ts | 4 + .../OAuthPageClientItem.utils.ts | 0 .../OAuthPageClientView.lazy.tsx | 15 + .../OAuthPageClientView.stories.tsx | 27 + .../OAuthPageClientView.test.tsx | 10 + .../OAuthPageClientView.tsx | 27 + .../OAuthPageClientView.types.ts | 5 + .../OAuthPageClientView.utils.ts | 28 + .../OAuthPageContainer.lazy.tsx | 15 + .../OAuthPageContainer.stories.tsx | 27 + .../OAuthPageContainer.test.tsx | 26 + .../OAuthPageContainer/OAuthPageContainer.tsx | 75 + .../OAuthPageContainer.types.ts | 10 + .../OAuthPageContainer.utils.ts | 0 .../OAuthPageDefaultView.lazy.tsx | 15 + .../OAuthPageDefaultView.stories.tsx | 27 + .../OAuthPageDefaultView.test.tsx | 10 + .../OAuthPageDefaultView.tsx | 22 + .../OAuthPageDefaultView.types.ts | 6 + .../OAuthPageDefaultView.utils.ts | 0 .../OAuthPageEditClientModal.lazy.tsx | 15 + .../OAuthPageEditClientModal.stories.tsx | 27 + .../OAuthPageEditClientModal.test.tsx | 10 + .../OAuthPageEditClientModal.tsx | 186 + .../OAuthPageEditClientModal.types.ts | 8 + .../OAuthPageEditClientModal.utils.ts | 0 .../OAuthPageNewClientButton.lazy.tsx | 15 + .../OAuthPageNewClientButton.stories.tsx | 27 + .../OAuthPageNewClientButton.test.tsx | 10 + .../OAuthPageNewClientButton.tsx | 37 + .../OAuthPageNewClientButton.types.ts | 14 + .../OAuthPageNewClientButton.utils.ts | 0 .../OAuthPageNewClientModal.lazy.tsx | 15 + .../OAuthPageNewClientModal.stories.tsx | 27 + .../OAuthPageNewClientModal.test.tsx | 10 + .../OAuthPageNewClientModal.tsx | 174 + .../OAuthPageNewClientModal.types.ts | 5 + .../OAuthPageNewClientModal.utils.ts | 0 .../CreateWebHookForm.lazy.tsx | 15 + .../CreateWebHookForm.schema.ts | 14 + .../CreateWebHookForm.stories.tsx | 27 + .../CreateWebHookForm/CreateWebHookForm.tsx | 323 + .../CreateWebHookForm.types.ts | 10 + .../CreateWebHookForm.utils.ts | 14 + .../WebHooksItem/WebHooksItem.lazy.tsx | 15 + .../WebHooksItem/WebHooksItem.stories.tsx | 38 + .../WebHooksItem/WebHooksItem.tsx | 21 + .../WebHooksItem/WebHooksItem.types.ts | 4 + .../WebHooksItem/WebHooksItem.utils.ts | 0 .../WebHooksPage/WebHooksPage.lazy.tsx | 15 + .../WebHooksPage/WebHooksPage.stories.tsx | 33 + .../WebHooksPage/WebHooksPage.test.tsx | 10 + .../WebHooksPage/WebHooksPage.tsx | 30 + .../WebHooksPage/WebHooksPage.types.ts | 3 + .../WebHooksPage/WebHooksPage.utils.ts | 30 + .../admin/AccessLogs/AccessLogs.lazy.tsx | 15 + .../admin/AccessLogs/AccessLogs.stories.tsx | 29 + .../07-pages/admin/AccessLogs/AccessLogs.tsx | 27 + .../admin/AccessLogs/AccessLogs.types.ts | 5 + .../admin/AccessLogs/AccessLogs.utils.ts | 22 + .../AccessLogsItem/AccessLogsItem.lazy.tsx | 15 + .../AccessLogsItem/AccessLogsItem.stories.tsx | 35 + .../AccessLogsItem/AccessLogsItem.tsx | 33 + .../AccessLogsItem/AccessLogsItem.types.ts | 4 + .../AccessLogsItem/AccessLogsItem.utils.ts | 0 .../07-pages/admin/ApiKeys/ApiKeys.lazy.tsx | 15 + .../admin/ApiKeys/ApiKeys.stories.tsx | 27 + .../07-pages/admin/ApiKeys/ApiKeys.test.tsx | 10 + .../07-pages/admin/ApiKeys/ApiKeys.tsx | 11 + .../07-pages/admin/ApiKeys/ApiKeys.types.ts | 5 + .../07-pages/admin/ApiKeys/ApiKeys.utils.ts | 5 + .../admin/Dashboard/Dashboard.lazy.tsx | 15 + .../admin/Dashboard/Dashboard.stories.tsx | 29 + .../07-pages/admin/Dashboard/Dashboard.tsx | 66 + .../admin/Dashboard/Dashboard.types.ts | 5 + .../admin/Dashboard/Dashboard.utils.ts | 0 .../DatabaseBackup/DatabaseBackup.lazy.tsx | 15 + .../DatabaseBackup/DatabaseBackup.stories.tsx | 29 + .../admin/DatabaseBackup/DatabaseBackup.tsx | 14 + .../DatabaseBackup/DatabaseBackup.types.ts | 5 + .../DatabaseBackup/DatabaseBackup.utils.ts | 0 .../EmailTemplateEdit.lazy.tsx | 15 + .../EmailTemplateEdit.stories.tsx | 27 + .../EmailTemplateEdit/EmailTemplateEdit.tsx | 193 + .../EmailTemplateEdit.types.ts | 5 + .../EmailTemplateEdit.utils.ts | 0 .../EmailTemplateItem.lazy.tsx | 15 + .../EmailTemplateItem.stories.tsx | 25 + .../EmailTemplateItem/EmailTemplateItem.tsx | 102 + .../EmailTemplateItem.types.ts | 4 + .../EmailTemplateItem.utils.ts | 0 .../EmailTemplateNew.lazy.tsx | 15 + .../EmailTemplateNew.stories.tsx | 27 + .../EmailTemplateNew/EmailTemplateNew.tsx | 171 + .../EmailTemplateNew.types.ts | 5 + .../EmailTemplateNew.utils.ts | 0 .../EmailTemplates/EmailTemplates.lazy.tsx | 15 + .../EmailTemplates/EmailTemplates.schema.ts | 3 + .../EmailTemplates/EmailTemplates.stories.tsx | 29 + .../admin/EmailTemplates/EmailTemplates.tsx | 42 + .../EmailTemplates/EmailTemplates.types.ts | 5 + .../EmailTemplates/EmailTemplates.utils.ts | 10 + .../07-pages/admin/Events/Events.lazy.tsx | 15 + .../07-pages/admin/Events/Events.stories.tsx | 29 + .../07-pages/admin/Events/Events.tsx | 25 + .../07-pages/admin/Events/Events.types.ts | 5 + .../07-pages/admin/Events/Events.utils.ts | 12 + .../Events/EventsItem/EventsItem.lazy.tsx | 15 + .../Events/EventsItem/EventsItem.stories.tsx | 34 + .../admin/Events/EventsItem/EventsItem.tsx | 24 + .../Events/EventsItem/EventsItem.types.ts | 4 + .../Events/EventsItem/EventsItem.utils.ts | 0 .../admin/IPNRecords/IPNRecords.lazy.tsx | 15 + .../admin/IPNRecords/IPNRecords.stories.tsx | 29 + .../07-pages/admin/IPNRecords/IPNRecords.tsx | 26 + .../admin/IPNRecords/IPNRecords.types.ts | 5 + .../admin/IPNRecords/IPNRecords.utils.ts | 34 + .../IPNRecordsItem/IPNRecordsItem.lazy.tsx | 15 + .../IPNRecordsItem/IPNRecordsItem.stories.tsx | 39 + .../IPNRecordsItem/IPNRecordsItem.tsx | 23 + .../IPNRecordsItem/IPNRecordsItem.types.ts | 4 + .../IPNRecordsItem/IPNRecordsItem.utils.ts | 0 .../07-pages/admin/Logs/Logs.lazy.tsx | 15 + .../07-pages/admin/Logs/Logs.stories.tsx | 29 + .../components/07-pages/admin/Logs/Logs.tsx | 14 + .../07-pages/admin/Logs/Logs.types.ts | 5 + .../07-pages/admin/Logs/Logs.utils.ts | 0 .../IssueGenuineCard.lazy.tsx | 15 + .../IssueGenuineCard.stories.tsx | 27 + .../IssueGenuineCard/IssueGenuineCard.tsx | 105 + .../IssueGenuineCard.types.ts | 5 + .../IssueGenuineCard.utils.ts | 0 .../ListGenuineCardPurchases.lazy.tsx | 15 + .../ListGenuineCardPurchases.stories.tsx | 27 + .../ListGenuineCardPurchases.tsx | 32 + .../ListGenuineCardPurchases.types.ts | 5 + .../ListGenuineCardPurchases.utils.ts | 48 + .../ListGenuineCardPurchasesItem.lazy.tsx | 17 + .../ListGenuineCardPurchasesItem.stories.tsx | 25 + .../ListGenuineCardPurchasesItem.tsx | 40 + .../ListGenuineCardPurchasesItem.types.ts | 4 + .../ListGenuineCardPurchasesItem.utils.ts | 0 .../ListGenuineCards.lazy.tsx | 15 + .../ListGenuineCards.stories.tsx | 27 + .../ListGenuineCards/ListGenuineCards.tsx | 27 + .../ListGenuineCards.types.ts | 5 + .../ListGenuineCards.utils.ts | 23 + .../ListGenuineCardsItem.lazy.tsx | 15 + .../ListGenuineCardsItem.stories.tsx | 25 + .../ListGenuineCardsItem.tsx | 26 + .../ListGenuineCardsItem.types.ts | 4 + .../ListGenuineCardsItem.utils.ts | 0 .../admin/MemberCards/MemberCards.lazy.tsx | 15 + .../admin/MemberCards/MemberCards.schema.ts | 12 + .../admin/MemberCards/MemberCards.stories.tsx | 29 + .../admin/MemberCards/MemberCards.tsx | 36 + .../admin/MemberCards/MemberCards.types.ts | 12 + .../admin/MemberCards/MemberCards.utils.ts | 0 .../RegisterGenuineCard.lazy.tsx | 15 + .../RegisterGenuineCard.stories.tsx | 27 + .../RegisterGenuineCard.tsx | 87 + .../RegisterGenuineCard.types.ts | 5 + .../RegisterGenuineCard.utils.ts | 0 .../admin/Memberships/Memberships.lazy.tsx | 15 + .../admin/Memberships/Memberships.schema.ts | 17 + .../admin/Memberships/Memberships.stories.tsx | 27 + .../admin/Memberships/Memberships.tsx | 27 + .../admin/Memberships/Memberships.types.ts | 9 + .../admin/Memberships/Memberships.utils.ts | 46 + .../MembershipsEdit/MembershipsEdit.lazy.tsx | 15 + .../MembershipsEdit.stories.tsx | 27 + .../MembershipsEdit/MembershipsEdit.test.tsx | 10 + .../MembershipsEdit/MembershipsEdit.tsx | 395 + .../MembershipsEdit/MembershipsEdit.types.ts | 5 + .../MembershipsEdit/MembershipsEdit.utils.ts | 0 .../MembershipsItem/MembershipsItem.lazy.tsx | 15 + .../MembershipsItem.stories.tsx | 25 + .../MembershipsItem/MembershipsItem.tsx | 52 + .../MembershipsItem/MembershipsItem.types.ts | 4 + .../MembershipsItem/MembershipsItem.utils.ts | 0 .../admin/Newsletter/Newsletter.lazy.tsx | 15 + .../admin/Newsletter/Newsletter.stories.tsx | 29 + .../07-pages/admin/Newsletter/Newsletter.tsx | 14 + .../admin/Newsletter/Newsletter.types.ts | 5 + .../admin/Newsletter/Newsletter.utils.ts | 0 .../07-pages/admin/OAuth/OAuth.lazy.tsx | 15 + .../07-pages/admin/OAuth/OAuth.stories.tsx | 29 + .../components/07-pages/admin/OAuth/OAuth.tsx | 9 + .../07-pages/admin/OAuth/OAuth.types.ts | 5 + .../07-pages/admin/OAuth/OAuth.utils.ts | 0 .../PaymentGateways/PaymentGateways.lazy.tsx | 15 + .../PaymentGateways.stories.tsx | 29 + .../admin/PaymentGateways/PaymentGateways.tsx | 14 + .../PaymentGateways/PaymentGateways.types.ts | 5 + .../PaymentGateways/PaymentGateways.utils.ts | 0 .../PayPalPaymentItem.lazy.tsx | 15 + .../PayPalPaymentItem.stories.tsx | 25 + .../PayPalPaymentItem/PayPalPaymentItem.tsx | 23 + .../PayPalPaymentItem.types.ts | 6 + .../PayPalPaymentItem.utils.ts | 0 .../07-pages/admin/Payments/Payments.lazy.tsx | 15 + .../admin/Payments/Payments.stories.tsx | 29 + .../07-pages/admin/Payments/Payments.tsx | 26 + .../07-pages/admin/Payments/Payments.types.ts | 5 + .../07-pages/admin/Payments/Payments.utils.ts | 12 + .../admin/Privileges/Privileges.lazy.tsx | 15 + .../admin/Privileges/Privileges.schema.ts | 3 + .../admin/Privileges/Privileges.stories.tsx | 29 + .../07-pages/admin/Privileges/Privileges.tsx | 26 + .../admin/Privileges/Privileges.types.ts | 10 + .../admin/Privileges/Privileges.utils.ts | 15 + .../PrivilegesEdit/PrivilegesEdit.lazy.tsx | 15 + .../PrivilegesEdit/PrivilegesEdit.stories.tsx | 27 + .../PrivilegesEdit/PrivilegesEdit.test.tsx | 10 + .../PrivilegesEdit/PrivilegesEdit.tsx | 177 + .../PrivilegesEdit/PrivilegesEdit.types.ts | 5 + .../PrivilegesEdit/PrivilegesEdit.utils.ts | 0 .../PrivilegesItem/PrivilegesItem.lazy.tsx | 15 + .../PrivilegesItem/PrivilegesItem.stories.tsx | 25 + .../PrivilegesItem/PrivilegesItem.tsx | 110 + .../PrivilegesItem/PrivilegesItem.types.ts | 6 + .../PrivilegesItem/PrivilegesItem.utils.ts | 0 .../07-pages/admin/Reports/Reports.lazy.tsx | 15 + .../admin/Reports/Reports.stories.tsx | 29 + .../07-pages/admin/Reports/Reports.tsx | 14 + .../07-pages/admin/Reports/Reports.types.ts | 5 + .../07-pages/admin/Reports/Reports.utils.ts | 0 .../SiteConfiguration.lazy.tsx | 15 + .../SiteConfiguration.stories.tsx | 29 + .../SiteConfiguration/SiteConfiguration.tsx | 14 + .../SiteConfiguration.types.ts | 5 + .../SiteConfiguration.utils.ts | 0 .../StripeRecords/StripeRecords.lazy.tsx | 15 + .../StripeRecords/StripeRecords.stories.tsx | 27 + .../admin/StripeRecords/StripeRecords.tsx | 26 + .../StripeRecords/StripeRecords.types.ts | 5 + .../StripeRecords/StripeRecords.utils.ts | 34 + .../StripeRecordsItem.lazy.tsx | 15 + .../StripeRecordsItem.stories.tsx | 39 + .../StripeRecordsItem/StripeRecordsItem.tsx | 22 + .../StripeRecordsItem.types.ts | 4 + .../StripeRecordsItem.utils.ts | 0 .../CreateSystemPreferenceButton.lazy.tsx | 17 + .../CreateSystemPreferenceButton.stories.tsx | 27 + .../CreateSystemPreferenceButton.tsx | 15 + .../CreateSystemPreferenceButton.types.ts | 5 + .../CreateSystemPreferenceButton.utils.ts | 0 .../SystemPreferences.lazy.tsx | 15 + .../SystemPreferences.schema.ts | 12 + .../SystemPreferences.stories.tsx | 27 + .../SystemPreferences.test.tsx | 10 + .../SystemPreferences/SystemPreferences.tsx | 31 + .../SystemPreferences.types.ts | 12 + .../SystemPreferences.utils.ts | 48 + .../SystemPreferencesEdit.lazy.tsx | 15 + .../SystemPreferencesEdit.stories.tsx | 27 + .../SystemPreferencesEdit.test.tsx | 10 + .../SystemPreferencesEdit.tsx | 254 + .../SystemPreferencesEdit.types.ts | 7 + .../SystemPreferencesEdit.utils.ts | 0 .../SystemPreferencesItem.lazy.tsx | 15 + .../SystemPreferencesItem.stories.tsx | 32 + .../SystemPreferencesItem.tsx | 99 + .../SystemPreferencesItem.types.ts | 6 + .../SystemPreferencesItem.utils.ts | 0 .../SystemPreferencesNew.lazy.tsx | 15 + .../SystemPreferencesNew.stories.tsx | 27 + .../SystemPreferencesNew.test.tsx | 10 + .../SystemPreferencesNew.tsx | 246 + .../SystemPreferencesNew.types.ts | 2 + .../SystemPreferencesNew.utils.ts | 0 .../CreateUserButton.lazy.tsx | 15 + .../CreateUserButton.stories.tsx | 21 + .../CreateUserButton/CreateUserButton.tsx | 15 + .../CreateUserButton.types.ts | 5 + .../CreateUserButton.utils.ts | 0 .../07-pages/admin/Users/Users.lazy.tsx | 15 + .../07-pages/admin/Users/Users.schema.ts | 83 + .../07-pages/admin/Users/Users.stories.tsx | 27 + .../components/07-pages/admin/Users/Users.tsx | 37 + .../07-pages/admin/Users/Users.types.ts | 19 + .../07-pages/admin/Users/Users.utils.ts | 107 + .../admin/Users/UsersEdit/UsersEdit.lazy.tsx | 15 + .../Users/UsersEdit/UsersEdit.stories.tsx | 27 + .../admin/Users/UsersEdit/UsersEdit.test.tsx | 10 + .../admin/Users/UsersEdit/UsersEdit.tsx | 653 + .../admin/Users/UsersEdit/UsersEdit.types.ts | 7 + .../admin/Users/UsersEdit/UsersEdit.utils.ts | 21 + .../admin/Users/UsersItem/UsersItem.lazy.tsx | 15 + .../Users/UsersItem/UsersItem.stories.tsx | 25 + .../admin/Users/UsersItem/UsersItem.tsx | 102 + .../admin/Users/UsersItem/UsersItem.types.ts | 18 + .../admin/Users/UsersItem/UsersItem.utils.ts | 0 .../admin/Users/UsersNew/UsersNew.lazy.tsx | 15 + .../admin/Users/UsersNew/UsersNew.stories.tsx | 27 + .../admin/Users/UsersNew/UsersNew.test.tsx | 10 + .../admin/Users/UsersNew/UsersNew.tsx | 176 + .../admin/Users/UsersNew/UsersNew.types.ts | 7 + .../admin/Users/UsersNew/UsersNew.utils.ts | 0 .../07-pages/admin/WebHooks/WebHooks.lazy.tsx | 15 + .../admin/WebHooks/WebHooks.stories.tsx | 27 + .../07-pages/admin/WebHooks/WebHooks.tsx | 9 + .../07-pages/admin/WebHooks/WebHooks.types.ts | 5 + .../07-pages/admin/WebHooks/WebHooks.utils.ts | 0 .../common/LandingPage/LandingPage.lazy.tsx | 15 + .../LandingPage/LandingPage.stories.tsx | 27 + .../common/LandingPage/LandingPage.tsx | 9 + .../common/LandingPage/LandingPage.types.ts | 5 + .../common/LandingPage/LandingPage.utils.ts | 0 .../07-pages/common/Login/Login.lazy.tsx | 15 + .../07-pages/common/Login/Login.schema.ts | 10 + .../07-pages/common/Login/Login.stories.tsx | 27 + .../07-pages/common/Login/Login.test.tsx | 10 + .../07-pages/common/Login/Login.tsx | 137 + .../07-pages/common/Login/Login.types.ts | 10 + .../07-pages/common/Login/Login.utils.ts | 0 .../user/AccessHistory/AccessHistory.lazy.tsx | 15 + .../AccessHistory/AccessHistory.stories.tsx | 27 + .../user/AccessHistory/AccessHistory.tsx | 30 + .../user/AccessHistory/AccessHistory.types.ts | 5 + .../user/AccessHistory/AccessHistory.utils.ts | 22 + .../AccessHistoryItem.lazy.tsx | 15 + .../AccessHistoryItem.stories.tsx | 35 + .../AccessHistoryItem/AccessHistoryItem.tsx | 34 + .../AccessHistoryItem.types.ts | 4 + .../AccessHistoryItem.utils.ts | 0 .../07-pages/user/ApiKeys/ApiKeys.lazy.tsx | 15 + .../07-pages/user/ApiKeys/ApiKeys.stories.tsx | 41 + .../07-pages/user/ApiKeys/ApiKeys.tsx | 9 + .../07-pages/user/ApiKeys/ApiKeys.types.ts | 5 + .../07-pages/user/ApiKeys/ApiKeys.utils.ts | 0 .../user/Dashboard/Dashboard.lazy.tsx | 15 + .../user/Dashboard/Dashboard.stories.tsx | 27 + .../07-pages/user/Dashboard/Dashboard.tsx | 225 + .../user/Dashboard/Dashboard.types.ts | 33 + .../user/Dashboard/Dashboard.utils.ts | 572 + .../user/DoorAccess/DoorAccess.lazy.tsx | 15 + .../user/DoorAccess/DoorAccess.stories.tsx | 27 + .../07-pages/user/DoorAccess/DoorAccess.tsx | 184 + .../user/DoorAccess/DoorAccess.types.ts | 5 + .../user/DoorAccess/DoorAccess.utils.ts | 0 .../user/GetInvolved/GetInvolved.guard.ts | 7 + .../user/GetInvolved/GetInvolved.lazy.tsx | 15 + .../user/GetInvolved/GetInvolved.stories.tsx | 27 + .../07-pages/user/GetInvolved/GetInvolved.tsx | 149 + .../user/GetInvolved/GetInvolved.types.ts | 5 + .../user/GetInvolved/GetInvolved.utils.ts | 5 + .../07-pages/user/Granting/Granting.lazy.tsx | 15 + .../07-pages/user/Granting/Granting.schema.ts | 14 + .../user/Granting/Granting.stories.tsx | 27 + .../07-pages/user/Granting/Granting.tsx | 262 + .../07-pages/user/Granting/Granting.types.ts | 10 + .../07-pages/user/Granting/Granting.utils.ts | 10 + .../GrantingItem/GrantingItem.lazy.tsx | 15 + .../GrantingItem/GrantingItem.stories.tsx | 27 + .../GrantingItem/GrantingItem.test.tsx | 12 + .../Granting/GrantingItem/GrantingItem.tsx | 110 + .../GrantingItem/GrantingItem.types.ts | 9 + .../GrantingItem/GrantingItem.utils.ts | 0 .../07-pages/user/Home/Home.lazy.tsx | 15 + .../07-pages/user/Home/Home.stories.tsx | 27 + .../components/07-pages/user/Home/Home.tsx | 109 + .../07-pages/user/Home/Home.types.ts | 5 + .../07-pages/user/Profile/APIKeysCard.tsx | 53 + .../user/Profile/LinkedAccountsCard.tsx | 64 + .../07-pages/user/Profile/PinCard.tsx | 123 + .../07-pages/user/Profile/Profile.lazy.tsx | 15 + .../07-pages/user/Profile/Profile.schema.ts | 19 + .../07-pages/user/Profile/Profile.stories.tsx | 27 + .../07-pages/user/Profile/Profile.tsx | 454 + .../07-pages/user/Profile/Profile.types.ts | 42 + .../07-pages/user/Profile/Profile.ui.tsx | 61 + .../07-pages/user/Profile/Profile.utils.ts | 0 .../07-pages/user/Profile/RFIDKeysCard.tsx | 46 + .../user/Profile/StandingCard.module.css | 7 + .../07-pages/user/Profile/StandingCard.tsx | 73 + .../user/Purchases/Purchases.lazy.tsx | 15 + .../user/Purchases/Purchases.stories.tsx | 27 + .../07-pages/user/Purchases/Purchases.tsx | 61 + .../user/Purchases/Purchases.types.ts | 5 + .../user/Purchases/Purchases.utils.ts | 0 .../TransactionItems.lazy.tsx | 15 + .../TransactionItems.stories.tsx | 25 + .../TransactionItems/TransactionItems.tsx | 33 + .../TransactionItems.types.ts | 4 + .../TransactionItems.utils.ts | 0 .../user/Transactions/Transactions.lazy.tsx | 15 + .../Transactions/Transactions.stories.tsx | 27 + .../user/Transactions/Transactions.tsx | 30 + .../user/Transactions/Transactions.types.ts | 5 + .../user/Transactions/Transactions.utils.ts | 23 + .../07-pages/user/WebHooks/WebHooks.lazy.tsx | 15 + .../user/WebHooks/WebHooks.stories.tsx | 27 + .../07-pages/user/WebHooks/WebHooks.tsx | 11 + .../07-pages/user/WebHooks/WebHooks.types.ts | 5 + .../07-pages/user/WebHooks/WebHooks.utils.ts | 0 .../src/components/08-app/App/App.lazy.tsx | 15 + .../08-app/App/App.react-router.tsx | 102 + .../src/components/08-app/App/App.stories.tsx | 27 + .../src/components/08-app/App/App.tsx | 39 + .../src/components/08-app/App/App.types.ts | 5 + .../src/components/08-app/App/App.utils.ts | 0 .../AuthenticationProvider.context.ts | 13 + .../AuthenticationProvider.lazy.tsx | 15 + .../AuthenticationProvider.settings.ts | 7 + .../AuthenticationProvider.stories.tsx | 25 + .../AuthenticationProvider.tsx | 76 + .../AuthenticationProvider.types.ts | 21 + .../AuthenticationProvider.utils.ts | 0 .../ConfigProvider/ConfigProvider.context.ts | 5 + .../ConfigProvider/ConfigProvider.guards.ts | 29 + .../ConfigProvider/ConfigProvider.hook.tsx | 16 + .../ConfigProvider/ConfigProvider.lazy.tsx | 15 + .../ConfigProvider/ConfigProvider.schemas.ts | 34 + .../ConfigProvider/ConfigProvider.stories.tsx | 27 + .../ConfigProvider/ConfigProvider.test.tsx | 10 + .../ConfigProvider/ConfigProvider.tsx | 81 + .../ConfigProvider/ConfigProvider.types.ts | 35 + .../ConfigProvider/ConfigProvider.utils.ts | 0 .../admin-page/AdminTemplateName.lazy.tsx | 15 + .../admin-page/AdminTemplateName.stories.tsx | 27 + .../admin-page/AdminTemplateName.test.tsx | 10 + .../admin-page/AdminTemplateName.tsx | 23 + .../admin-page/AdminTemplateName.types.ts | 5 + .../admin-page/AdminTemplateName.utils.ts | 5 + .../item/AdminTemplateNameItem.lazy.tsx | 15 + .../item/AdminTemplateNameItem.stories.tsx | 25 + .../admin-page/item/AdminTemplateNameItem.tsx | 14 + .../item/AdminTemplateNameItem.types.ts | 3 + .../item/AdminTemplateNameItem.utils.ts | 0 .../default/TemplateName.lazy.tsx | 15 + .../default/TemplateName.stories.tsx | 27 + .../default/TemplateName.test.tsx | 10 + .../99-templates/default/TemplateName.tsx | 7 + .../default/TemplateName.types.ts | 5 + .../default/TemplateName.utils.ts | 0 .../99-templates/page/TemplateName.lazy.tsx | 15 + .../page/TemplateName.stories.tsx | 27 + .../99-templates/page/TemplateName.test.tsx | 10 + .../99-templates/page/TemplateName.tsx | 7 + .../99-templates/page/TemplateName.types.ts | 5 + .../99-templates/page/TemplateName.utils.ts | 0 .../user-page/TemplateName.lazy.tsx | 15 + .../user-page/TemplateName.stories.tsx | 27 + .../user-page/TemplateName.test.tsx | 10 + .../99-templates/user-page/TemplateName.tsx | 7 + .../user-page/TemplateName.types.ts | 5 + .../user-page/TemplateName.utils.ts | 0 .../UserTemplateNameItem.lazy.tsx | 15 + .../UserTemplateNameItem.stories.tsx | 27 + .../UserTemplateNameItem.tsx | 9 + .../UserTemplateNameItem.types.ts | 5 + .../UserTemplateNameItem.utils.ts | 0 packages/frontend-react/src/lib/backend.ts | 12 + .../src/lib/db/models/PrincipalUser.ts | 9 + .../frontend-react/src/lib/db/models/User.ts | 108 + .../src/lib/db/utils/query-filters.ts | 75 + .../src/lib/exceptions/HTTPException.ts | 5 + packages/frontend-react/src/lib/fetcher.ts | 164 + .../frontend-react/src/lib/guards/common.ts | 468 + .../frontend-react/src/lib/guards/records.ts | 470 + .../ApiKeyService2/useDeleteApiKey.tsx | 4 + .../useGenerateSystemApiKey.tsx | 4 + .../ApiKeyService2/useGenerateUserApiKey.tsx | 4 + .../providers/ApiKeyService2/useGetApiKey.tsx | 8 + .../ApiKeyService2/useGetSystemApiKeys.tsx | 20 + .../ApiKeyService2/useGetUserApiKeys.tsx | 2 + .../ApiKeyService2/usePutApiKeyPrivileges.tsx | 4 + .../ApiKeyService2/useUpdateApiKey.tsx | 4 + .../providers/AuthService2/useCheckPin.tsx | 2 + .../providers/AuthService2/useCheckRfid.tsx | 2 + .../AuthService2/useCheckService.tsx | 2 + .../AuthService2/useCheckUsername.tsx | 2 + .../AuthService2/useCountAccessLog.tsx | 4 + .../AuthService2/useCountUserAccessLog.tsx | 29 + .../providers/AuthService2/useCurrentUser.tsx | 66 + .../providers/AuthService2/useGetUser.tsx | 2 + .../AuthService2/useListAccessLog.tsx | 4 + .../AuthService2/useListUserAccessLog.tsx | 44 + .../hooks/providers/AuthService2/useLogin.tsx | 4 + .../providers/AuthService2/useLogout.tsx | 4 + .../providers/AuthService2/usePinLogin.tsx | 2 + .../providers/AuthService2/useRfidLogin.tsx | 2 + .../CurrentUser/useGetCurrentUser.tsx | 4 + .../CurrentUser2/useGetCurrentUser.tsx | 2 + .../EmailService2/useCountTemplates.tsx | 4 + .../EmailService2/useDeleteTemplate.tsx | 4 + .../providers/EmailService2/useEmail.tsx | 2 + .../providers/EmailService2/useEmailUser.tsx | 2 + .../EmailService2/useGetTemplate.tsx | 11 + .../EmailService2/useListTemplates.tsx | 4 + .../EmailService2/usePutTemplate.tsx | 4 + .../EmailService2/useUpdateTemplate.tsx | 2 + .../EmailService2/useUpdateTemplateBody.tsx | 2 + .../EmailService2/useUpdateTemplateCode.tsx | 2 + .../EmailService2/useUpdateTemplateHelp.tsx | 2 + .../EmailService2/useUpdateTemplateHtml.tsx | 2 + .../EmailService2/useUpdateTemplateName.tsx | 2 + .../useUpdateTemplateSubject.tsx | 2 + .../EventService2/useCountEvents.tsx | 4 + .../EventService2/useCreateEvent.tsx | 4 + .../EventService2/useDeleteEvent.tsx | 4 + .../EventService2/useEnableEvent.tsx | 4 + .../EventService2/useGetAccessibleEvents.tsx | 4 + .../EventService2/useGetDomainDefinition.tsx | 2 + .../EventService2/useGetDomainDefinitions.tsx | 4 + .../providers/EventService2/useGetEvent.tsx | 4 + .../EventService2/useGetEventTypes.tsx | 4 + .../providers/EventService2/useGetEvents.tsx | 2 + .../providers/EventService2/useListEvents.tsx | 4 + .../EventService2/usePutEventPrivileges.tsx | 4 + .../EventService2/useUpdateEvent.tsx | 4 + .../providers/IpnService2/useCountRecords.tsx | 4 + .../hooks/providers/IpnService2/useGet.tsx | 4 + .../hooks/providers/IpnService2/useGetAll.tsx | 4 + .../providers/IpnService2/useListRecords.tsx | 4 + .../providers/KeyService2/useDeleteKey.tsx | 2 + .../KeyService2/useGenerateUserKey.tsx | 2 + .../providers/KeyService2/useGetAllKeys.tsx | 2 + .../hooks/providers/KeyService2/useGetKey.tsx | 2 + .../KeyService2/useGetSystemKeys.tsx | 2 + .../providers/KeyService2/useGetUserKeys.tsx | 2 + .../KeyService2/usePutKeyPrivileges.tsx | 2 + .../providers/KeyService2/useUpdateKey.tsx | 2 + .../useCountGenuineCards.tsx | 2 + .../useCountUserGenuineCards.tsx | 2 + .../useGetGenuineCardDetails.tsx | 4 + .../MemberCardService2/useIssueCard.tsx | 4 + .../useListGenuineCards.tsx | 4 + .../useListUserGenuineCards.tsx | 4 + .../useRegisterGenuineCard.tsx | 4 + .../useUpdateGenuineCardActive.tsx | 4 + .../useValidateGenuineCard.tsx | 4 + .../useCountMemberships.tsx | 2 + .../MembershipService2/useCreate.tsx | 2 + .../providers/MembershipService2/useGet.tsx | 4 + .../MembershipService2/useGetAll.tsx | 9 + .../MembershipService2/useListMemberships.tsx | 2 + .../MembershipService2/usePutPrivileges.tsx | 2 + .../MembershipService2/useUpdate.tsx | 4 + .../MembershipService2/useUpdateActive.tsx | 4 + .../MembershipService2/useUpdatePrivate.tsx | 4 + .../MembershipService2/useUpdateRecurring.tsx | 4 + .../MembershipService2/useUpdateTrial.tsx | 4 + .../MetricService2/useGetCreatedDates.tsx | 42 + .../useGetExceptionPayments.tsx | 4 + .../MetricService2/useGetMembers.tsx | 46 + .../MetricService2/useGetNewKeyHolders.tsx | 4 + .../MetricService2/useGetNewMembers.tsx | 39 + .../MetricService2/useGetPendingAccounts.tsx | 4 + .../MetricService2/useGetRevenue.tsx | 56 + .../MetricService2/useGetTotalKeyHolders.tsx | 41 + .../MetricService2/useGetTotalMembers.tsx | 41 + .../OAuthService2/useCountClients.tsx | 2 + .../OAuthService2/useCountUserClients.tsx | 2 + .../OAuthService2/useDeleteClient.tsx | 2 + .../OAuthService2/useEnableClient.tsx | 2 + .../OAuthService2/useGetAccessToken.tsx | 2 + .../providers/OAuthService2/useGetClient.tsx | 7 + .../OAuthService2/useGetClientDetails.tsx | 7 + .../OAuthService2/useGetClientInfo.tsx | 7 + .../OAuthService2/useGetRefreshToken.tsx | 2 + .../OAuthService2/useListClients.tsx | 2 + .../OAuthService2/useListUserClients.tsx | 2 + .../OAuthService2/useRegisterClient.tsx | 2 + .../OAuthService2/useRevokeRefreshToken.tsx | 2 + .../OAuthService2/useSaveAccessToken.tsx | 2 + .../OAuthService2/useSaveRefreshToken.tsx | 2 + .../OAuthService2/useUpdateClient.tsx | 2 + .../OAuthService2/useUpdateClientExpiry.tsx | 2 + .../PaymentService2/useCountPayments.tsx | 4 + .../PaymentService2/useCountUserPayments.tsx | 4 + .../PaymentService2/useGetPayment.tsx | 4 + .../PaymentService2/useListPayments.tsx | 4 + .../PaymentService2/useListUserPayments.tsx | 4 + .../useReplayPaymentProcessing.tsx | 4 + .../PinService2/useAccessInstructions.tsx | 2 + .../providers/PinService2/useGeneratePin.tsx | 4 + .../PinService2/useGenerateTemporaryPin.tsx | 2 + .../providers/PinService2/useGetUserPin.tsx | 4 + .../providers/PinService2/useUpdatePin.tsx | 4 + .../PinService2/useUpdateUserPin.tsx | 4 + .../useCountSystemPreferences.tsx | 2 + .../useDeleteSystemPreference.tsx | 4 + .../useGetAllSystemPreferences.tsx | 4 + .../useGetSystemPreference.tsx | 18 + .../useListSystemPreferences.tsx | 4 + .../usePutSystemPreference.tsx | 4 + .../usePutSystemPreferencePrivileges.tsx | 4 + .../useSystemPreference.tsx | 4 + .../useUpdateSystemPreference.tsx | 4 + .../useUpdateSystemPreferenceEnabled.tsx | 4 + .../PrivilegeService2/useCountPrivileges.tsx | 2 + .../PrivilegeService2/useCreatePrivilege.tsx | 4 + .../PrivilegeService2/useDeletePrivilege.tsx | 4 + .../PrivilegeService2/useGetAllPrivileges.tsx | 29 + .../useGetAllSystemPermissions.tsx | 4 + .../PrivilegeService2/useGetPrivilege.tsx | 11 + .../useGetUserPrivileges.tsx | 33 + .../PrivilegeService2/useListPrivileges.tsx | 2 + .../useUpdatePrivilegeDescription.tsx | 4 + .../useUpdatePrivilegeEnabled.tsx | 4 + .../useUpdatePrivilegeIcon.tsx | 4 + .../useUpdatePrivilegeName.tsx | 4 + .../StripeEventService2/useCountRecords.tsx | 4 + .../providers/StripeEventService2/useGet.tsx | 4 + .../StripeEventService2/useGetAll.tsx | 4 + .../StripeEventService2/useListRecords.tsx | 4 + .../useCountSystemPreferences.tsx | 2 + .../useDeleteSystemPreference.tsx | 2 + .../useGetAllSystemPreferences.tsx | 2 + .../useGetSystemPreference.tsx | 2 + .../useListSystemPreferences.tsx | 2 + .../usePutSystemPreference.tsx | 2 + .../usePutSystemPreferencePrivileges.tsx | 2 + .../useSystemPreference.tsx | 2 + .../useUpdateSystemPreference.tsx | 2 + .../useUpdateSystemPreferenceEnabled.tsx | 2 + .../providers/UserService2/useCountUsers.tsx | 47 + .../providers/UserService2/useCreate.tsx | 4 + .../providers/UserService2/useGetStanding.tsx | 11 + .../providers/UserService2/useGetStatuses.tsx | 2 + .../providers/UserService2/useGetUser.tsx | 44 + .../useGetUserGrantablePrivileges.tsx | 38 + .../providers/UserService2/useGetUsers.tsx | 4 + .../UserService2/useGrantPrivilege.tsx | 4 + .../providers/UserService2/useListUsers.tsx | 76 + .../UserService2/usePutUserPrivileges.tsx | 2 + .../providers/UserService2/useRegister.tsx | 4 + .../UserService2/useRequestPasswordReset.tsx | 4 + .../UserService2/useRequestSlackInvite.tsx | 4 + .../UserService2/useResetPassword.tsx | 4 + .../UserService2/useRevokePrivilege.tsx | 4 + .../providers/UserService2/useUpdateCash.tsx | 4 + .../providers/UserService2/useUpdateEmail.tsx | 4 + .../UserService2/useUpdateExpiry.tsx | 4 + .../UserService2/useUpdateMembership.tsx | 4 + .../providers/UserService2/useUpdateName.tsx | 4 + .../UserService2/useUpdateNewsletter.tsx | 4 + .../UserService2/useUpdatePassword.tsx | 4 + .../UserService2/useUpdatePaymentEmail.tsx | 4 + .../UserService2/useUpdateStatus.tsx | 4 + .../UserService2/useUpdateStripeEmail.tsx | 4 + .../UserService2/useUpdateUsername.tsx | 4 + .../WebHookService2/useCountHooks.tsx | 4 + .../WebHookService2/useCountUserHooks.tsx | 4 + .../WebHookService2/useCreateHook.tsx | 4 + .../WebHookService2/useDeleteHook.tsx | 4 + .../WebHookService2/useEnableHook.tsx | 4 + .../WebHookService2/useGetAllHooks.tsx | 2 + .../providers/WebHookService2/useGetHook.tsx | 4 + .../providers/WebHookService2/useGetHooks.tsx | 2 + .../WebHookService2/useListHooks.tsx | 4 + .../WebHookService2/useListUserHooks.tsx | 4 + .../WebHookService2/usePutHookPrivileges.tsx | 4 + .../WebHookService2/useUpdateHook.tsx | 4 + .../frontend-react/src/lib/hooks/useAuth.tsx | 14 + .../src/lib/hooks/useClickOutside.tsx | 30 + .../src/lib/hooks/useInterval.tsx | 24 + .../lib/hooks/usePrivilegeCodesReducer.tsx | 72 + .../src/lib/hooks/useSelectorReducer.tsx | 72 + .../src/lib/hooks/useToggleReducer.tsx | 168 + .../frontend-react/src/lib/mocking/data.ts | 131 + .../src/lib/mocking/functions.ts | 10 + .../src/lib/mocking/handlers.ts | 66 + packages/frontend-react/src/lib/nomos.ts | 63 + .../src/lib/providers/ApiKeyService2.ts | 110 + .../src/lib/providers/AuthService2.ts | 219 + .../src/lib/providers/CurrentUser2.ts | 22 + .../src/lib/providers/EmailService2.ts | 258 + .../src/lib/providers/EventService2.ts | 203 + .../src/lib/providers/IpnService2.ts | 73 + .../src/lib/providers/KeyService2.ts | 111 + .../src/lib/providers/MemberCardService2.ts | 159 + .../src/lib/providers/MembershipService2.ts | 202 + .../src/lib/providers/MetricService2.ts | 137 + .../src/lib/providers/OAuthService2.ts | 317 + .../src/lib/providers/PaymentService2.ts | 123 + .../src/lib/providers/PinService2.ts | 92 + .../src/lib/providers/PreferenceService2.ts | 179 + .../src/lib/providers/PrivilegeService2.ts | 185 + .../src/lib/providers/StripeEventService2.ts | 79 + .../lib/providers/SystemPreferenceService2.ts | 184 + .../src/lib/providers/UserService2.ts | 360 + .../src/lib/providers/WebHookService2.ts | 239 + packages/frontend-react/src/lib/ui/common.tsx | 9 + .../src/lib/ui/fontawesome/common.ts | 25 + .../src/lib/ui/fontawesome/index.ts | 2 + packages/frontend-react/src/lib/ui/index.ts | 1 + .../src/lib/ui/storybook/common.tsx | 21 + .../src/lib/ui/storybook/index.ts | 1 + .../frontend-react/src/lib/utils/common.ts | 226 + .../frontend-react/src/lib/utils/constants.ts | 8 + .../frontend-react/src/lib/utils/debugging.ts | 48 + .../frontend-react/src/lib/utils/index.ts | 2 + .../src/lib/validators/common.ts | 210 + .../src/lib/validators/custom.ts | 0 .../src/lib/validators/records.ts | 492 + packages/frontend-react/src/main.css | 17 + packages/frontend-react/src/main.tsx | 21 + packages/frontend-react/src/routeTree.gen.ts | 1911 +++ packages/frontend-react/src/router.tsx | 19 + packages/frontend-react/src/routes/__root.tsx | 14 + packages/frontend-react/src/routes/_admin.tsx | 18 + .../src/routes/_admin/admin.accesslogs.tsx | 7 + .../src/routes/_admin/admin.apikeys.tsx | 7 + .../src/routes/_admin/admin.dashboard.tsx | 7 + .../routes/_admin/admin.databasebackup.tsx | 7 + .../src/routes/_admin/admin.email.tsx | 7 + .../admin.emailtemplates.$templateId.tsx | 7 + .../_admin/admin.emailtemplates.new.tsx | 7 + .../routes/_admin/admin.emailtemplates.tsx | 7 + .../src/routes/_admin/admin.events.tsx | 7 + .../src/routes/_admin/admin.index.tsx | 17 + .../src/routes/_admin/admin.ipnrecords.tsx | 7 + .../src/routes/_admin/admin.logs.tsx | 7 + .../src/routes/_admin/admin.membercards.tsx | 7 + .../admin.memberships.$membershipId.tsx | 7 + .../src/routes/_admin/admin.memberships.tsx | 7 + .../src/routes/_admin/admin.newsletter.tsx | 7 + .../src/routes/_admin/admin.oauth.$.tsx | 7 + .../src/routes/_admin/admin.oauth.tsx | 7 + .../routes/_admin/admin.paymentgateways.tsx | 7 + .../src/routes/_admin/admin.payments.tsx | 7 + .../_admin/admin.privileges.$privilegeId.tsx | 7 + .../src/routes/_admin/admin.privileges.tsx | 7 + .../src/routes/_admin/admin.reports.tsx | 7 + .../routes/_admin/admin.siteconfiguration.tsx | 7 + .../src/routes/_admin/admin.striperecords.tsx | 7 + .../src/routes/_admin/admin.systemkeys.$.tsx | 7 + .../routes/_admin/admin.systemkeys.index.tsx | 7 + .../admin.systempreferences.$preferenceId.tsx | 7 + .../_admin/admin.systempreferences.new.tsx | 7 + .../routes/_admin/admin.systempreferences.tsx | 7 + .../src/routes/_admin/admin.transactions.tsx | 7 + .../src/routes/_admin/admin.users.$userId.tsx | 7 + .../src/routes/_admin/admin.users.new.tsx | 7 + .../src/routes/_admin/admin.users.tsx | 7 + .../src/routes/_admin/admin.webhooks.tsx | 7 + .../src/routes/_admin/backup.tsx | 7 + .../src/routes/_admin/config.tsx | 7 + .../src/routes/_admin/events.tsx | 7 + .../frontend-react/src/routes/_admin/logs.tsx | 7 + .../src/routes/_admin/oauth.tsx | 7 + .../frontend-react/src/routes/_public.tsx | 7 + .../src/routes/_public/login.tsx | 14 + .../routes/_public/recovery.reset.$token.tsx | 15 + .../src/routes/_public/recovery.tsx | 15 + .../src/routes/_public/register.tsx | 15 + packages/frontend-react/src/routes/_user.tsx | 18 + .../src/routes/_user/accesslogs.tsx | 7 + .../src/routes/_user/apikeys.$.tsx | 7 + .../src/routes/_user/apikeys.index.tsx | 7 + .../src/routes/_user/dashboard.tsx | 7 + .../src/routes/_user/dooraccess.tsx | 7 + .../src/routes/_user/getinvolved.tsx | 7 + .../src/routes/_user/grants.tsx | 7 + .../frontend-react/src/routes/_user/home.tsx | 7 + .../frontend-react/src/routes/_user/index.tsx | 23 + .../src/routes/_user/logout.tsx | 14 + .../src/routes/_user/membership.tsx | 15 + .../src/routes/_user/profile.tsx | 7 + .../src/routes/_user/purchase.tsx | 7 + .../src/routes/_user/settings.tsx | 15 + .../src/routes/_user/transactions.tsx | 7 + .../src/routes/_user/vhsopen.tsx | 15 + .../src/routes/_user/webhooks.tsx | 7 + .../frontend-react/src/styles/base/common.css | 63 + .../frontend-react/src/styles/base/tables.css | 62 + .../src/styles/components/btn.css | 109 + .../src/styles/components/card.css | 98 + .../src/styles/components/charts.css | 5 + .../src/styles/components/list-group.css | 17 + .../styles/components/overlay-container.css | 9 + .../src/styles/components/popover.css | 9 + .../src/styles/components/tabs.css | 13 + .../frontend-react/src/styles/tailwind.css | 3 + .../src/styles/utilities/common.css | 72 + .../src/styles/utilities/find-me.css | 45 + .../src/styles/utilities/layout.css | 43 + .../src/styles/utilities/tables.css | 6 + packages/frontend-react/src/types/api.ts | 31 + packages/frontend-react/src/types/charts.ts | 8 + packages/frontend-react/src/types/common.ts | 17 + packages/frontend-react/src/types/db.ts | 6 + .../src/types/fontawesome/common.ts | 37 + .../src/types/fontawesome/index.ts | 2 + .../src/types/providers/IApiKeyService2.ts | 81 + .../src/types/providers/IAuthService2.ts | 172 + .../src/types/providers/IEmailService2.ts | 197 + .../src/types/providers/IEventService2.ts | 152 + .../src/types/providers/IIpnService2.ts | 53 + .../src/types/providers/IKeyService2.ts | 82 + .../types/providers/IMemberCardService2.ts | 116 + .../types/providers/IMembershipService2.ts | 147 + .../src/types/providers/IMetricService2.ts | 106 + .../src/types/providers/IOAuthService2.ts | 238 + .../src/types/providers/IPaymentService2.ts | 86 + .../src/types/providers/IPinService2.ts | 67 + .../types/providers/IPreferenceService2.ts | 127 + .../src/types/providers/IPrivilegeService2.ts | 137 + .../types/providers/IStripeEventService2.ts | 53 + .../providers/ISystemPreferenceService2.ts | 129 + .../src/types/providers/IUserService2.ts | 283 + .../src/types/providers/IWebHookService2.ts | 177 + packages/frontend-react/src/types/routing.ts | 3 + packages/frontend-react/src/types/ui.ts | 31 + packages/frontend-react/src/types/utils.ts | 15 + .../src/types/validators/common.ts | 320 + .../src/types/validators/records.ts | 326 + packages/frontend-react/src/vite-env.d.ts | 14 + packages/frontend-react/stylelint.config.mjs | 16 + packages/frontend-react/tailwind.config.js | 153 + .../frontend-react/tools/bootstrap-config.sh | 5 + .../tools/clean-build-assets.sh | 29 + .../tools/fix-storybook-titles.sh | 11 + .../frontend-react/tools/generate-fa-names.sh | 30 + .../frontend-react/tools/generate-fa-types.sh | 33 + .../generate-provider-implementations.sh | 105 + .../generate-validator-implementations.sh | 81 + packages/frontend-react/tsconfig.eslint.json | 5 + packages/frontend-react/tsconfig.json | 35 + packages/frontend-react/tsconfig.node.json | 12 + packages/frontend-react/tsr.config.json | 6 + packages/frontend-react/vite.config.ts | 82 + .bowerrc => packages/frontend-web/.bowerrc | 0 packages/frontend-web/.prettierignore | 21 + .../frontend-web/bower.json | 0 .../conf/nginx-vhost-docker-compose.conf | 28 + .../frontend-web/conf/nginx-vhost-docker.conf | 29 + .../conf/nginx-vhost-windows.conf | 54 + packages/frontend-web/justfile | 33 + packages/frontend-web/package.json | 33 + .../frontend-web/tools}/bower.sh | 2 +- .../web}/admin/accesslogs/accesslogs.html | 0 .../web}/admin/accesslogs/accesslogs.js | 0 .../frontend-web/web}/admin/admin.html | 0 .../frontend-web/web}/admin/admin.js | 0 .../web}/admin/apikeys/apikeys.html | 0 .../web}/admin/apikeys/apikeys.js | 0 .../web}/admin/apikeys/details.html | 0 .../web}/admin/apikeys/details.js | 0 .../web}/admin/backup/backup.html | 0 .../frontend-web/web}/admin/backup/backup.js | 0 .../web}/admin/config/config.html | 0 .../frontend-web/web}/admin/config/config.js | 0 .../frontend-web/web}/admin/email/email.html | 0 .../frontend-web/web}/admin/email/email.js | 0 .../web}/admin/events/events.html | 0 .../frontend-web/web}/admin/events/events.js | 0 .../frontend-web/web}/admin/home/home.html | 0 .../frontend-web/web}/admin/home/home.js | 0 .../frontend-web/web}/admin/ipn/records.html | 0 .../frontend-web/web}/admin/ipn/records.js | 0 .../frontend-web/web}/admin/logs/logs.html | 0 .../frontend-web/web}/admin/logs/logs.js | 0 .../web}/admin/membercards/membercards.html | 0 .../web}/admin/membercards/membercards.js | 0 .../web}/admin/memberships/edit.html | 0 .../web}/admin/memberships/edit.js | 0 .../web}/admin/memberships/memberships.html | 0 .../web}/admin/memberships/memberships.js | 0 .../web}/admin/oauth/clients.html | 0 .../frontend-web/web}/admin/oauth/clients.js | 0 .../frontend-web/web}/admin/oauth/oauth.html | 0 .../frontend-web/web}/admin/oauth/oauth.js | 0 .../web}/admin/payments/payments.html | 0 .../web}/admin/payments/payments.js | 0 .../web}/admin/privileges/privileges.html | 0 .../web}/admin/privileges/privileges.js | 0 .../web}/admin/stripe/records.html | 0 .../frontend-web/web}/admin/stripe/records.js | 0 .../systempreferences/systempreferences.html | 0 .../systempreferences/systempreferences.js | 0 .../web}/admin/transactions/transactions.html | 0 .../web}/admin/transactions/transactions.js | 0 .../web}/admin/users/profile.html | 0 .../frontend-web/web}/admin/users/profile.js | 0 .../frontend-web/web}/admin/users/users.html | 0 .../frontend-web/web}/admin/users/users.js | 0 .../web}/admin/webhooks/webhooks.html | 0 .../web}/admin/webhooks/webhooks.js | 0 {web => packages/frontend-web/web}/app.css | 0 {web => packages/frontend-web/web}/app.js | 0 .../web}/apple-touch-icon-precomposed.png | Bin .../frontend-web/web}/apple-touch-icon.png | Bin .../web/badges/cert_cnc_mill_lathe.svg | 74 + .../frontend-web/web/badges/cert_laser.svg | 32 + .../frontend-web/web/badges/door_access.svg | 8 + .../frontend-web/web/badges/key_holder.svg | 140 + .../web/badges/key_holder_inactive.svg | 54 + packages/frontend-web/web/badges/laser.png | Bin 0 -> 28133 bytes .../frontend-web/web/badges/newsletter.svg | 28 + packages/frontend-web/web/favicon.ico | Bin 0 -> 1150 bytes .../gateways/moneybookers/MoneyBookers.png | 0 .../moneybookers/MoneyBookers_big.png | 0 .../web}/gateways/paypal/PayPal.png | 0 .../web}/gateways/paypal/PayPal_big.png | 0 packages/frontend-web/web/images/logo.png | Bin 0 -> 4492 bytes .../web/images/provider/github.png | Bin 0 -> 3404 bytes .../web/images/provider/google.png | Bin 0 -> 3895 bytes .../frontend-web/web/images/provider/pin.png | Bin 0 -> 619 bytes .../frontend-web/web/images/provider/rfid.png | Bin 0 -> 802 bytes .../web/images/provider/slack.png | Bin 0 -> 5071 bytes packages/frontend-web/web/images/slack.png | Bin 0 -> 5071 bytes .../images/vhs-member-card-2015-2-full.png | Bin 0 -> 598913 bytes .../images/vhs-member-card-2015-2-thumb.png | Bin 0 -> 52368 bytes .../web/images/vhs-member-card-2015-full.png | Bin 0 -> 679719 bytes .../web/images/vhs-member-card-2015-thumb.png | Bin 0 -> 56963 bytes {web => packages/frontend-web/web}/index.html | 0 .../frontend-web/web}/providers/apikey.js | 0 .../frontend-web/web}/providers/auth.js | 0 .../web}/providers/currentUser.js | 0 .../frontend-web/web}/providers/email.js | 0 .../frontend-web/web}/providers/event.js | 0 .../frontend-web/web}/providers/ipn.js | 0 .../frontend-web/web}/providers/membercard.js | 0 .../frontend-web/web}/providers/membership.js | 0 .../frontend-web/web}/providers/metric.js | 0 .../frontend-web/web}/providers/payment.js | 0 .../frontend-web/web}/providers/pin.js | 0 .../frontend-web/web}/providers/preference.js | 0 .../frontend-web/web}/providers/privilege.js | 0 .../frontend-web/web}/providers/stripe.js | 0 .../frontend-web/web}/providers/user.js | 0 .../frontend-web/web}/providers/webhook.js | 0 .../frontend-web/web}/public/login/login.html | 0 .../frontend-web/web}/public/login/login.js | 0 .../frontend-web/web}/public/public.js | 0 .../web}/public/recovery/recovery.html | 0 .../web}/public/recovery/recovery.js | 0 .../web}/public/recovery/reset.html | 0 .../web}/public/recovery/reset.js | 0 .../web}/public/register/register.html | 0 .../web}/public/register/register.js | 0 {web => packages/frontend-web/web}/setup/img | 0 .../frontend-web/web}/setup/license.html | 0 .../frontend-web/web}/setup/style.css | 0 .../web}/user/accesslogs/accesslogs.html | 0 .../web}/user/accesslogs/accesslogs.js | 0 .../web}/user/apikeys/apikeys.html | 0 .../frontend-web/web}/user/apikeys/apikeys.js | 0 .../web}/user/apikeys/details.html | 0 .../frontend-web/web}/user/apikeys/details.js | 0 .../web}/user/dashboard/dashboard.html | 0 .../web}/user/dashboard/dashboard.js | 0 .../web}/user/dooraccess/dooraccess.html | 0 .../web}/user/dooraccess/dooraccess.js | 0 .../web}/user/getinvolved/getinvolved.html | 0 .../web}/user/getinvolved/getinvolved.js | 0 .../frontend-web/web}/user/grants/grants.html | 0 .../frontend-web/web}/user/grants/grants.js | 0 packages/frontend-web/web/user/home/home.html | 0 .../frontend-web/web}/user/home/home.js | 0 .../web}/user/membership/membership.html | 0 .../web}/user/membership/membership.js | 0 .../web}/user/profile/profile.html | 0 .../frontend-web/web}/user/profile/profile.js | 0 .../web}/user/purchase/purchase.html | 0 .../web}/user/purchase/purchase.js | 0 .../web}/user/settings/settings.html | 0 .../web}/user/settings/settings.js | 0 .../web}/user/transactions/transactions.html | 0 .../web}/user/transactions/transactions.js | 0 .../frontend-web/web}/user/user.html | 0 .../frontend-web/web}/user/user.js | 0 .../web}/user/vhsopen/vhsopen.html | 0 .../frontend-web/web}/user/vhsopen/vhsopen.js | 0 .../web}/user/webhooks/webhooks.html | 0 .../web}/user/webhooks/webhooks.js | 0 pnpm-lock.yaml | 12557 ++++++++++++++++ prettier.config.mjs | 37 +- tests/ConstantsTest.php | 49 - tests/schema/RingSchema.php | 37 - tests/schema/SwordEnchantmentsSchema.php | 36 - tools/ghetto_deploy.sh | 160 - tools/make-webhook-key.sh | 34 - tools/vagrant_provision.sh | 136 - tsconfig.json | 42 - vhs/Logger.php | 18 - vhs/database/IDataInterface.php | 37 - vhs/database/ITableGenerator.php | 14 - vhs/database/access/IAccess.php | 19 - vhs/database/joins/IJoinGenerator.php | 24 - vhs/database/joins/Join.php | 64 - vhs/database/offsets/Offset.php | 27 - vhs/database/queries/IQueryGenerator.php | 24 - vhs/database/queries/Query.php | 86 - vhs/database/types/ITypeConverter.php | 30 - vhs/database/types/ITypeGenerator.php | 30 - vhs/database/wheres/IWhereGenerator.php | 20 - vhs/domain/Filter.php | 70 - vhs/domain/collections/DomainCollection.php | 93 - vhs/messaging/ConnectionInfo.php | 18 - vhs/messaging/Engine.php | 24 - vhs/messaging/IMessagingInterface.php | 22 - vhs/messaging/MessageQueue.php | 125 - .../RabbitMQ/RabbitMQConnectionInfo.php | 60 - .../engines/RabbitMQ/RabbitMQEngine.php | 129 - .../exceptions/MessageQueueException.php | 13 - vhs/monitors/Monitor.php | 17 - vhs/security/IPrincipal.php | 26 - .../exceptions/InvalidCredentials.php | 13 - .../exceptions/UnauthorizedException.php | 16 - vhs/services/Service.php | 19 - vhs/services/endpoints/IEndpoint.php | 20 - .../exceptions/InvalidRequestException.php | 13 - vhs/web/IHttpModule.php | 18 - .../modules/HttpJsonServiceHandlerModule.php | 60 - web/assets | 1 - web/cache | 1 - web/theme | 1 - web/uploads | 1 - webhooker/.gitignore | 2 - webhooker/README.md | 3 - webhooker/config.docker.js | 16 - webhooker/config.js.template.js | 16 - webhooker/domains.js | 75 - webhooker/events.js | 104 - webhooker/nomos.js | 113 - webhooker/package-lock.json | 2058 --- webhooker/package.json | 28 - webhooker/test/domains.js | 547 - webhooker/test/webhooks.js | 47 - webhooker/webhooker.console | 7 - webhooker/webhooker.js | 72 - webhooker/webhooker.sbin | 7 - webhooker/webhooker.service | 10 - webhooker/webhooks.js | 149 - 2101 files changed, 78403 insertions(+), 9891 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/run-tests.yml create mode 100755 .husky/pre-push delete mode 100644 Dockerfile delete mode 100644 Vagrantfile delete mode 100644 app/constants/DateTime.php delete mode 100644 app/constants/Errors.php delete mode 100644 app/domain/AppClient.php delete mode 100644 app/domain/EmailTemplate.php delete mode 100644 app/domain/Event.php delete mode 100644 app/domain/GenuineCard.php delete mode 100644 app/domain/Ipn.php delete mode 100644 app/domain/Key.php delete mode 100644 app/domain/PasswordResetRequest.php delete mode 100644 app/domain/Payment.php delete mode 100644 app/domain/StripeEvent.php delete mode 100644 app/domain/WebHook.php delete mode 100644 app/exceptions/InvalidInputException.php delete mode 100644 app/exceptions/InvalidPasswordHashException.php delete mode 100644 app/exceptions/MemberCardException.php delete mode 100644 app/exceptions/UserAlreadyExistsException.php delete mode 100644 app/monitors/DomainEventMonitor.php delete mode 100644 app/schema/AccessTokenSchema.php delete mode 100644 app/schema/EventPrivilegeSchema.php delete mode 100644 app/schema/KeyPrivilegeSchema.php delete mode 100644 app/schema/MembershipPrivilegeSchema.php delete mode 100644 app/schema/RefreshTokenSchema.php delete mode 100644 app/schema/SystemPreferencePrivilegeSchema.php delete mode 100644 app/schema/UserPrivilegeSchema.php delete mode 100644 app/schema/WebHookPrivilegeSchema.php delete mode 100644 app/security/oauth/providers/slack/SlackProviderException.php delete mode 100644 circle.yml delete mode 100644 composer.json delete mode 100644 conf/config.ini.php.template delete mode 100644 conf/mimes.types delete mode 100644 conf/nginx-vhost-docker-compose.conf delete mode 100644 conf/nginx-vhost-vagrant.conf delete mode 100644 conf/nginx-vhost-windows.conf delete mode 100644 docker-compose.dev.conf delete mode 100644 docker-compose.sample.conf delete mode 100755 docker-compose.sh delete mode 100644 docker-compose.template.conf create mode 100644 docker-compose.yml create mode 100644 docker-compose/.dockerignore delete mode 100644 docker-compose/build-backend.yml delete mode 100644 docker-compose/build-frontend.yml delete mode 100644 docker-compose/core.network-bridge.yml delete mode 100644 docker-compose/core.network-proxy-internal.yml delete mode 100644 docker-compose/core.network-proxy.yml delete mode 100644 docker-compose/core.ports.yml delete mode 100644 docker-compose/core.yml delete mode 100644 docker-compose/files-local-backend.yml delete mode 100644 docker-compose/files-local-frontend.yml delete mode 100644 docker-compose/files-local-nomos-env.yml delete mode 100644 docker-compose/mysql-external-mysqld.yml delete mode 100644 docker-compose/mysql-external-network-mysql-lithium.yml delete mode 100644 docker-compose/mysql-external-network-mysql.yml delete mode 100644 docker-compose/mysql-external-variable.yml delete mode 100644 docker-compose/mysql-local-network-bridge.yml delete mode 100644 docker-compose/mysql-local-network-mysql.yml delete mode 100644 docker-compose/mysql-local.yml delete mode 100644 docker-compose/nginx-proxy-devtest.yml delete mode 100644 docker-compose/nginx-proxy-prod.yml rename {docker => docker-compose}/nomos.env.template (63%) delete mode 100644 docker-compose/override.yml delete mode 100644 docker-compose/rabbitmq-external.yml delete mode 100644 docker-compose/rabbitmq-local-management.yml delete mode 100644 docker-compose/rabbitmq-local-network-bridge.yml delete mode 100644 docker-compose/rabbitmq-local-network-internal.yml delete mode 100644 docker-compose/rabbitmq-local-network-rabbitmq.yml delete mode 100644 docker-compose/rabbitmq-local.yml delete mode 100644 docker-compose/traefik-devtest.yml delete mode 100644 docker-compose/traefik-prod.yml delete mode 100644 docker-compose/webhooker-build.yml delete mode 100644 docker-compose/webhooker-logs-local.yml delete mode 100644 docker-compose/webhooker-network-bridge.yml delete mode 100644 docker-compose/webhooker-network-proxy.yml delete mode 100644 docker-compose/webhooker-network-rabbitmq.yml delete mode 100644 docker-compose/webhooker.yml delete mode 100755 docker/docker_compose_run.sh delete mode 100755 docker/docker_env_config.sh delete mode 100644 eslint.config.mjs delete mode 100644 nomos.d.ts rename .php-cs-fixer.php => packages/backend-php/.php-cs-fixer.php (85%) create mode 100644 packages/backend-php/app/adapters/v2/EmailAdapter2.php rename {app => packages/backend-php/app}/app.php (88%) create mode 100644 packages/backend-php/app/constants/Errors.php create mode 100644 packages/backend-php/app/constants/Formats.php rename {app => packages/backend-php/app}/constants/StringLiterals.php (93%) rename {app => packages/backend-php/app}/contracts/IApiKeyService1.php (79%) rename {app => packages/backend-php/app}/contracts/IAuthService1.php (66%) rename {app => packages/backend-php/app}/contracts/IEmailService1.php (61%) rename {app => packages/backend-php/app}/contracts/IEventService1.php (70%) rename {app => packages/backend-php/app}/contracts/IIpnService1.php (64%) rename {app => packages/backend-php/app}/contracts/IKeyService1.php (76%) rename {app => packages/backend-php/app}/contracts/IMemberCardService1.php (59%) rename {app => packages/backend-php/app}/contracts/IMembershipService1.php (56%) rename {app => packages/backend-php/app}/contracts/IMetricService1.php (91%) rename {app => packages/backend-php/app}/contracts/IPaymentService1.php (57%) rename {app => packages/backend-php/app}/contracts/IPinService1.php (80%) rename {app => packages/backend-php/app}/contracts/IPreferenceService1.php (66%) rename {app => packages/backend-php/app}/contracts/IPrivilegeService1.php (71%) rename {app => packages/backend-php/app}/contracts/IStripeEventService1.php (64%) rename {app => packages/backend-php/app}/contracts/IUserService1.php (67%) rename {app => packages/backend-php/app}/contracts/IWebHookService1.php (56%) create mode 100644 packages/backend-php/app/contracts/v2/IApiKeyService2.php create mode 100644 packages/backend-php/app/contracts/v2/IAuthService2.php create mode 100644 packages/backend-php/app/contracts/v2/IEmailService2.php create mode 100644 packages/backend-php/app/contracts/v2/IEventService2.php create mode 100644 packages/backend-php/app/contracts/v2/IIpnService2.php create mode 100644 packages/backend-php/app/contracts/v2/IKeyService2.php create mode 100644 packages/backend-php/app/contracts/v2/IMemberCardService2.php create mode 100644 packages/backend-php/app/contracts/v2/IMembershipService2.php create mode 100644 packages/backend-php/app/contracts/v2/IMetricService2.php create mode 100644 packages/backend-php/app/contracts/v2/IOAuthService2.php create mode 100644 packages/backend-php/app/contracts/v2/IPaymentService2.php create mode 100644 packages/backend-php/app/contracts/v2/IPinService2.php create mode 100644 packages/backend-php/app/contracts/v2/IPreferenceService2.php create mode 100644 packages/backend-php/app/contracts/v2/IPrivilegeService2.php create mode 100644 packages/backend-php/app/contracts/v2/IStripeEventService2.php create mode 100644 packages/backend-php/app/contracts/v2/ISystemPreferenceService2.php create mode 100644 packages/backend-php/app/contracts/v2/IUserService2.php create mode 100644 packages/backend-php/app/contracts/v2/IWebHookService2.php rename {app => packages/backend-php/app}/domain/AccessLog.php (58%) rename {app => packages/backend-php/app}/domain/AccessToken.php (56%) create mode 100644 packages/backend-php/app/domain/AppClient.php create mode 100644 packages/backend-php/app/domain/EmailTemplate.php create mode 100644 packages/backend-php/app/domain/Event.php create mode 100644 packages/backend-php/app/domain/GenuineCard.php create mode 100644 packages/backend-php/app/domain/Ipn.php create mode 100644 packages/backend-php/app/domain/Key.php rename {app => packages/backend-php/app}/domain/Membership.php (61%) create mode 100644 packages/backend-php/app/domain/PasswordResetRequest.php create mode 100644 packages/backend-php/app/domain/Payment.php rename {app => packages/backend-php/app}/domain/Privilege.php (52%) rename {app => packages/backend-php/app}/domain/RefreshToken.php (53%) create mode 100644 packages/backend-php/app/domain/StripeEvent.php rename {app => packages/backend-php/app}/domain/SystemPreference.php (65%) rename {app => packages/backend-php/app}/domain/User.php (57%) create mode 100644 packages/backend-php/app/domain/WebHook.php create mode 100644 packages/backend-php/app/dto/AccessLog.php create mode 100644 packages/backend-php/app/dto/AccessToken.php create mode 100644 packages/backend-php/app/dto/AppClient.php create mode 100644 packages/backend-php/app/dto/AppClientInfo.php create mode 100644 packages/backend-php/app/dto/EmailTemplate.php create mode 100644 packages/backend-php/app/dto/Event.php create mode 100644 packages/backend-php/app/dto/GeneratedEmailResults.php create mode 100644 packages/backend-php/app/dto/GenuineCard.php create mode 100644 packages/backend-php/app/dto/Ipn.php create mode 100644 packages/backend-php/app/dto/IpnValidationEnum.php create mode 100644 packages/backend-php/app/dto/Key.php create mode 100644 packages/backend-php/app/dto/KeyTypeEnum.php create mode 100644 packages/backend-php/app/dto/Membership.php create mode 100644 packages/backend-php/app/dto/PasswordResetRequest.php create mode 100644 packages/backend-php/app/dto/Payment.php create mode 100644 packages/backend-php/app/dto/PaymentPpEnum.php create mode 100644 packages/backend-php/app/dto/Privilege.php create mode 100644 packages/backend-php/app/dto/RefreshToken.php create mode 100644 packages/backend-php/app/dto/SavedRefreshToken.php create mode 100644 packages/backend-php/app/dto/ServiceResponse.php create mode 100644 packages/backend-php/app/dto/ServiceResponseError.php create mode 100644 packages/backend-php/app/dto/ServiceResponseErrorInvalidToken.php create mode 100644 packages/backend-php/app/dto/ServiceResponseErrorUserNotFoundByEmailAddress.php create mode 100644 packages/backend-php/app/dto/ServiceResponseSuccess.php create mode 100644 packages/backend-php/app/dto/StripeEvent.php create mode 100644 packages/backend-php/app/dto/StripeEventStatusEnum.php create mode 100644 packages/backend-php/app/dto/SystemPreference.php create mode 100644 packages/backend-php/app/dto/TotalKeyHoldersResult.php create mode 100644 packages/backend-php/app/dto/TotalMembersResult.php create mode 100644 packages/backend-php/app/dto/TrimmedAppClient.php create mode 100644 packages/backend-php/app/dto/TrimmedAppClientOwner.php create mode 100644 packages/backend-php/app/dto/TrimmedUser.php create mode 100644 packages/backend-php/app/dto/User.php create mode 100644 packages/backend-php/app/dto/UserActiveEnum.php create mode 100644 packages/backend-php/app/dto/UserPrincipal.php create mode 100644 packages/backend-php/app/dto/WebHook.php create mode 100644 packages/backend-php/app/dto/v2/MetricServiceGetCreatedDatesResult.php create mode 100644 packages/backend-php/app/dto/v2/MetricServiceGetMembersResult.php create mode 100644 packages/backend-php/app/dto/v2/MetricServiceGetRevenueResult.php create mode 100644 packages/backend-php/app/dto/v2/MetricServiceNewKeyholdersResult.php create mode 100644 packages/backend-php/app/dto/v2/MetricServiceNewMembersResult.php create mode 100644 packages/backend-php/app/dto/v2/MetricServiceTotalKeyHoldersResult.php create mode 100644 packages/backend-php/app/dto/v2/MetricServiceTotalMembersResult.php create mode 100644 packages/backend-php/app/dto/v2/MetricServiceValueResult.php create mode 100644 packages/backend-php/app/endpoints/v2/ApiKeyService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/AuthService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/EmailService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/EventService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/IpnService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/KeyService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/MemberCardService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/MembershipService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/MetricService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/OAuthService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/PaymentService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/PinService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/PreferenceService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/PrivilegeService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/StripeEventService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/SystemPreferenceService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/UserService2.svc.php create mode 100644 packages/backend-php/app/endpoints/v2/WebHookService2.svc.php rename {app => packages/backend-php/app}/endpoints/web/ApiKeyService1.svc.php (95%) rename {app => packages/backend-php/app}/endpoints/web/AuthService1.svc.php (95%) rename {app => packages/backend-php/app}/endpoints/web/EmailService1.svc.php (95%) rename {app => packages/backend-php/app}/endpoints/web/EventService1.svc.php (95%) rename {app => packages/backend-php/app}/endpoints/web/IpnService1.svc.php (94%) rename {app => packages/backend-php/app}/endpoints/web/KeyService1.svc.php (95%) rename {app => packages/backend-php/app}/endpoints/web/MemberCardService1.svc.php (95%) rename {app => packages/backend-php/app}/endpoints/web/MembershipService1.svc.php (94%) rename {app => packages/backend-php/app}/endpoints/web/MetricService1.svc.php (94%) rename {app => packages/backend-php/app}/endpoints/web/PaymentService1.svc.php (94%) rename {app => packages/backend-php/app}/endpoints/web/PinService1.svc.php (95%) rename {app => packages/backend-php/app}/endpoints/web/PreferenceService1.svc.php (95%) rename {app => packages/backend-php/app}/endpoints/web/PrivilegeService1.svc.php (95%) rename {app => packages/backend-php/app}/endpoints/web/StripeEventService1.svc.php (94%) rename {app => packages/backend-php/app}/endpoints/web/UserService1.svc.php (95%) rename {app => packages/backend-php/app}/endpoints/web/WebHookService1.svc.php (95%) create mode 100644 packages/backend-php/app/enums/MetricServiceGroupType.php create mode 100644 packages/backend-php/app/exceptions/InvalidAccessTokenCredentialsException.php create mode 100644 packages/backend-php/app/exceptions/InvalidInputException.php create mode 100644 packages/backend-php/app/exceptions/InvalidKeyCredentialsException.php create mode 100644 packages/backend-php/app/exceptions/InvalidPasswordHashException.php create mode 100644 packages/backend-php/app/exceptions/MemberCardException.php create mode 100644 packages/backend-php/app/exceptions/UserAlreadyExistsException.php rename {app => packages/backend-php/app}/gateways/IPaymentGateway.php (55%) rename {app => packages/backend-php/app}/gateways/PaymentGatewayException.php (90%) rename {app => packages/backend-php/app}/gateways/PaypalGateway.php (79%) rename {app => packages/backend-php/app}/gateways/StripeGateway.php (66%) create mode 100644 packages/backend-php/app/handlers/v2/ApiKeyServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/AuthServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/EmailServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/EventServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/IpnServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/KeyServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/MemberCardServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/MembershipServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/MetricServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/OAuthServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/PaymentServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/PinServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/PreferenceServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/PrivilegeServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/StripeEventServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/SystemPreferenceServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/UserServiceHandler2.php create mode 100644 packages/backend-php/app/handlers/v2/WebHookServiceHandler2.php rename {app => packages/backend-php/app}/include.php (54%) rename {app => packages/backend-php/app}/modules/HttpPaymentGatewayHandler.php (80%) rename {app => packages/backend-php/app}/modules/HttpPaymentGatewayHandlerModule.php (80%) rename {app => packages/backend-php/app}/monitors/PaymentMonitor.php (76%) rename {app => packages/backend-php/app}/monitors/PaypalIpnMonitor.php (90%) rename {app => packages/backend-php/app}/monitors/StripeEventMonitor.php (91%) rename {app => packages/backend-php/app}/processors/PaymentProcessor.php (83%) rename {app => packages/backend-php/app}/resources/Resource.php (76%) rename {app => packages/backend-php/app}/schema/AccessLogSchema.php (84%) create mode 100644 packages/backend-php/app/schema/AccessTokenSchema.php rename {app => packages/backend-php/app}/schema/AppClientSchema.php (63%) rename {app => packages/backend-php/app}/schema/EmailSchema.php (90%) create mode 100644 packages/backend-php/app/schema/EventPrivilegeSchema.php rename {app => packages/backend-php/app}/schema/EventSchema.php (90%) rename {app => packages/backend-php/app}/schema/GenuineCardSchema.php (86%) rename {app => packages/backend-php/app}/schema/IpnSchema.php (92%) create mode 100644 packages/backend-php/app/schema/KeyPrivilegeSchema.php rename {app => packages/backend-php/app}/schema/KeySchema.php (61%) create mode 100644 packages/backend-php/app/schema/MembershipPrivilegeSchema.php rename {app => packages/backend-php/app}/schema/MembershipSchema.php (92%) rename {app => packages/backend-php/app}/schema/PasswordResetRequestSchema.php (57%) rename {app => packages/backend-php/app}/schema/PaymentSchema.php (59%) rename {app => packages/backend-php/app}/schema/PrivilegeSchema.php (90%) create mode 100644 packages/backend-php/app/schema/RefreshTokenSchema.php rename {app => packages/backend-php/app}/schema/SettingsSchema.php (99%) rename {app => packages/backend-php/app}/schema/StripeEventSchema.php (91%) create mode 100644 packages/backend-php/app/schema/SystemPreferencePrivilegeSchema.php rename {app => packages/backend-php/app}/schema/SystemPreferenceSchema.php (89%) create mode 100644 packages/backend-php/app/schema/UserPrivilegeSchema.php rename {app => packages/backend-php/app}/schema/UserSchema.php (70%) create mode 100644 packages/backend-php/app/schema/WebHookPrivilegeSchema.php rename {app => packages/backend-php/app}/schema/WebHookSchema.php (52%) rename {app => packages/backend-php/app}/security/Authenticate.php (69%) rename {app => packages/backend-php/app}/security/ColumnPrivilegedAccess.php (60%) rename {app => packages/backend-php/app}/security/HttpApiAuthModule.php (71%) rename {app => packages/backend-php/app}/security/PasswordUtil.php (67%) rename {app => packages/backend-php/app}/security/PrivilegedAccess.php (68%) rename {app => packages/backend-php/app}/security/TablePrivilegedAccess.php (57%) rename {app => packages/backend-php/app}/security/TokenPrincipal.php (55%) rename {app => packages/backend-php/app}/security/UserPrincipal.php (57%) rename {app => packages/backend-php/app}/security/credentials/ApiCredentials.php (92%) rename {app => packages/backend-php/app}/security/credentials/PinCredentials.php (92%) rename {app => packages/backend-php/app}/security/credentials/RfidCredentials.php (92%) rename {app => packages/backend-php/app}/security/credentials/TokenCredentials.php (60%) rename {app => packages/backend-php/app}/security/oauth/OAuthHelper.php (71%) rename {app => packages/backend-php/app}/security/oauth/modules/GithubOAuthHandler.php (79%) rename {app => packages/backend-php/app}/security/oauth/modules/GoogleOAuthHandler.php (83%) rename {app => packages/backend-php/app}/security/oauth/modules/OAuthHandler.php (77%) rename {app => packages/backend-php/app}/security/oauth/modules/OAuthHandlerModule.php (79%) rename {app => packages/backend-php/app}/security/oauth/modules/SlackOAuthHandler.php (85%) rename {app => packages/backend-php/app}/security/oauth/providers/slack/Slack.php (50%) create mode 100644 packages/backend-php/app/security/oauth/providers/slack/SlackProviderException.php rename {app => packages/backend-php/app}/security/oauth/providers/slack/SlackUser.php (52%) rename {app => packages/backend-php/app}/services/ApiKeyService.php (76%) rename {app => packages/backend-php/app}/services/AuthService.php (76%) rename {app => packages/backend-php/app}/services/EmailService.php (66%) rename {app => packages/backend-php/app}/services/EventService.php (80%) rename {app => packages/backend-php/app}/services/IpnService.php (72%) rename {app => packages/backend-php/app}/services/KeyService.php (77%) rename {app => packages/backend-php/app}/services/MemberCardService.php (67%) rename {app => packages/backend-php/app}/services/MembershipService.php (64%) rename {app => packages/backend-php/app}/services/MetricService.php (76%) rename {app => packages/backend-php/app}/services/PaymentService.php (71%) rename {app => packages/backend-php/app}/services/PinService.php (68%) rename {app => packages/backend-php/app}/services/PreferenceService.php (75%) rename {app => packages/backend-php/app}/services/PrivilegeService.php (78%) rename {app => packages/backend-php/app}/services/StripeEventService.php (73%) rename {app => packages/backend-php/app}/services/UserService.php (83%) rename {app => packages/backend-php/app}/services/WebHookService.php (73%) create mode 100644 packages/backend-php/app/utils/AuthCheckResult.php create mode 100644 packages/backend-php/app/utils/DTO.php create mode 100644 packages/backend-php/app/utils/EnumMapper.php create mode 100644 packages/backend-php/app/utils/IDTO.php create mode 100644 packages/backend-php/app/utils/converters/PHP2TS.php create mode 100644 packages/backend-php/composer.json rename composer.lock => packages/backend-php/composer.lock (64%) rename {conf => packages/backend-php/conf}/config.docker.ini.php (84%) create mode 100644 packages/backend-php/conf/config.ini.template.php rename conf/nginx-vhost-docker.conf => packages/backend-php/conf/nginx.conf (68%) create mode 100644 packages/backend-php/conf/php-fpm/00-defaults.conf create mode 100644 packages/backend-php/conf/php-fpm/99-no-daemonize.conf create mode 100644 packages/backend-php/conf/php/99-no-fastcgi-logging.ini create mode 100644 packages/backend-php/conf/php/99-opcache.ini create mode 100644 packages/backend-php/conf/php/99-sessions-dir.ini create mode 100755 packages/backend-php/docker/docker_compose_run.sh create mode 100755 packages/backend-php/docker/docker_env_config.sh create mode 100755 packages/backend-php/docker/docker_env_config_local.sh rename {docker => packages/backend-php/docker}/docker_run.sh (71%) create mode 100644 packages/backend-php/justfile create mode 100644 packages/backend-php/package.json create mode 100644 packages/backend-php/php-ts-transformer.php create mode 100644 packages/backend-php/phpstan.neon rename phpunit.xml => packages/backend-php/phpunit.xml (100%) create mode 100644 packages/backend-php/psalm.xml create mode 100644 packages/backend-php/tests/ConstantsTest.php rename {tests => packages/backend-php/tests}/DomainTest.php (76%) rename {tests => packages/backend-php/tests}/EmailTemplateDomainTest.php (83%) rename {tests => packages/backend-php/tests}/KeyDomainTest.php (87%) rename {tests => packages/backend-php/tests}/LimitTest.php (74%) rename {tests => packages/backend-php/tests}/ServiceTest.php (70%) rename {tests => packages/backend-php/tests}/WhereTest.php (84%) rename {tests => packages/backend-php/tests}/autoload.php (70%) rename {tests => packages/backend-php/tests}/contracts/ITestService1.php (88%) rename {tests => packages/backend-php/tests}/domain/Enchantment.php (73%) create mode 100644 packages/backend-php/tests/domain/ExampleDomain.php rename {tests => packages/backend-php/tests}/domain/Knight.php (59%) rename {tests => packages/backend-php/tests}/domain/Ring.php (55%) rename {tests => packages/backend-php/tests}/domain/Sword.php (73%) rename {tests => packages/backend-php/tests}/endpoints/native/TestService1.svc.php (95%) rename {tests => packages/backend-php/tests}/endpoints/web/TestService1.svc.php (95%) rename {tests => packages/backend-php/tests}/schema/EnchantmentSchema.php (87%) create mode 100644 packages/backend-php/tests/schema/ExampleSchema.php rename {tests => packages/backend-php/tests}/schema/KnightSchema.php (54%) create mode 100644 packages/backend-php/tests/schema/RingSchema.php create mode 100644 packages/backend-php/tests/schema/SwordEnchantmentsSchema.php rename {tests => packages/backend-php/tests}/schema/SwordSchema.php (86%) create mode 100644 packages/backend-php/tests/security/PermPrincipal.php rename {tests => packages/backend-php/tests}/services/TestService.php (90%) rename {tools => packages/backend-php/tools}/composer.sh (100%) create mode 100755 packages/backend-php/tools/generate-dto-domain-classes.php create mode 100644 packages/backend-php/tools/generate-dto-schema-classes.php create mode 100755 packages/backend-php/tools/generate-ts-contract-implementation.php create mode 100755 packages/backend-php/tools/generate-ts-contract-interface.php rename {tools => packages/backend-php/tools}/migrate.php (86%) create mode 100755 packages/backend-php/tools/sync-dto-classes.sh create mode 100644 packages/backend-php/vhs/BasePath.php rename {vhs => packages/backend-php/vhs}/Cloneable.php (77%) create mode 100644 packages/backend-php/vhs/Logger.php create mode 100644 packages/backend-php/vhs/Loggington.php rename {vhs => packages/backend-php/vhs}/Singleton.php (62%) rename {vhs => packages/backend-php/vhs}/SplClassLoader.php (76%) rename {vhs => packages/backend-php/vhs}/database/Column.php (66%) rename {vhs => packages/backend-php/vhs}/database/Columns.php (65%) rename {vhs => packages/backend-php/vhs}/database/ConnectionInfo.php (64%) rename {vhs => packages/backend-php/vhs}/database/Database.php (68%) rename {vhs => packages/backend-php/vhs}/database/Element.php (66%) rename {vhs => packages/backend-php/vhs}/database/Engine.php (57%) rename {vhs => packages/backend-php/vhs}/database/IColumnGenerator.php (61%) rename {vhs => packages/backend-php/vhs}/database/IConverter.php (88%) rename {vhs => packages/backend-php/vhs}/database/IConvertible.php (94%) create mode 100644 packages/backend-php/vhs/database/IDataInterface.php rename {vhs => packages/backend-php/vhs}/database/IGeneratable.php (69%) rename {vhs => packages/backend-php/vhs}/database/IGenerator.php (88%) rename {vhs => packages/backend-php/vhs}/database/IOnGenerator.php (62%) create mode 100644 packages/backend-php/vhs/database/ITableGenerator.php rename {vhs => packages/backend-php/vhs}/database/On.php (54%) rename {vhs => packages/backend-php/vhs}/database/Table.php (69%) create mode 100644 packages/backend-php/vhs/database/access/IAccess.php rename {vhs => packages/backend-php/vhs}/database/access/IAccessGenerator.php (89%) rename {vhs => packages/backend-php/vhs}/database/constraints/Constraint.php (53%) rename {vhs => packages/backend-php/vhs}/database/constraints/ForeignKey.php (87%) rename {vhs => packages/backend-php/vhs}/database/constraints/IConstraintGenerator.php (53%) rename {vhs => packages/backend-php/vhs}/database/constraints/PrimaryKey.php (94%) rename {vhs => packages/backend-php/vhs}/database/engines/memory/InMemoryEngine.php (73%) rename {vhs => packages/backend-php/vhs}/database/engines/memory/InMemoryGenerator.php (62%) rename {vhs => packages/backend-php/vhs}/database/engines/mysql/MySqlConnectionInfo.php (54%) rename {vhs => packages/backend-php/vhs}/database/engines/mysql/MySqlConverter.php (66%) rename {vhs => packages/backend-php/vhs}/database/engines/mysql/MySqlEngine.php (68%) rename {vhs => packages/backend-php/vhs}/database/engines/mysql/MySqlGenerator.php (76%) rename {vhs => packages/backend-php/vhs}/database/exceptions/DatabaseConnectionException.php (91%) rename {vhs => packages/backend-php/vhs}/database/exceptions/DatabaseException.php (90%) rename {vhs => packages/backend-php/vhs}/database/exceptions/InvalidSchemaException.php (90%) create mode 100644 packages/backend-php/vhs/database/joins/IJoinGenerator.php create mode 100644 packages/backend-php/vhs/database/joins/Join.php rename {vhs => packages/backend-php/vhs}/database/joins/JoinCross.php (93%) rename {vhs => packages/backend-php/vhs}/database/joins/JoinInner.php (93%) rename {vhs => packages/backend-php/vhs}/database/joins/JoinLeft.php (93%) rename {vhs => packages/backend-php/vhs}/database/joins/JoinOuter.php (93%) rename {vhs => packages/backend-php/vhs}/database/joins/JoinRight.php (93%) rename {vhs => packages/backend-php/vhs}/database/limits/ILimitGenerator.php (54%) rename {vhs => packages/backend-php/vhs}/database/limits/Limit.php (54%) rename {vhs => packages/backend-php/vhs}/database/offsets/IOffsetGenerator.php (54%) create mode 100644 packages/backend-php/vhs/database/offsets/Offset.php rename {vhs => packages/backend-php/vhs}/database/orders/IOrderByGenerator.php (53%) rename {vhs => packages/backend-php/vhs}/database/orders/OrderBy.php (60%) rename {vhs => packages/backend-php/vhs}/database/orders/OrderByAscending.php (65%) rename {vhs => packages/backend-php/vhs}/database/orders/OrderByDescending.php (65%) create mode 100644 packages/backend-php/vhs/database/queries/IQueryGenerator.php create mode 100644 packages/backend-php/vhs/database/queries/Query.php rename {vhs => packages/backend-php/vhs}/database/queries/QueryCount.php (56%) rename {vhs => packages/backend-php/vhs}/database/queries/QueryDelete.php (64%) rename {vhs => packages/backend-php/vhs}/database/queries/QueryInsert.php (71%) rename {vhs => packages/backend-php/vhs}/database/queries/QuerySelect.php (55%) rename {vhs => packages/backend-php/vhs}/database/queries/QueryUpdate.php (63%) create mode 100644 packages/backend-php/vhs/database/types/ITypeConverter.php create mode 100644 packages/backend-php/vhs/database/types/ITypeGenerator.php rename {vhs => packages/backend-php/vhs}/database/types/Type.php (56%) rename {vhs => packages/backend-php/vhs}/database/types/TypeBool.php (79%) rename {vhs => packages/backend-php/vhs}/database/types/TypeDate.php (79%) rename {vhs => packages/backend-php/vhs}/database/types/TypeDateTime.php (79%) rename {vhs => packages/backend-php/vhs}/database/types/TypeEnum.php (61%) rename {vhs => packages/backend-php/vhs}/database/types/TypeFloat.php (79%) rename {vhs => packages/backend-php/vhs}/database/types/TypeInt.php (78%) rename {vhs => packages/backend-php/vhs}/database/types/TypeString.php (69%) rename {vhs => packages/backend-php/vhs}/database/types/TypeText.php (79%) create mode 100644 packages/backend-php/vhs/database/wheres/IWhereGenerator.php rename {vhs => packages/backend-php/vhs}/database/wheres/Where.php (55%) rename {vhs => packages/backend-php/vhs}/database/wheres/WhereAnd.php (57%) rename {vhs => packages/backend-php/vhs}/database/wheres/WhereComparator.php (59%) rename {vhs => packages/backend-php/vhs}/database/wheres/WhereOr.php (86%) rename {vhs => packages/backend-php/vhs}/domain/Domain.php (72%) rename {vhs => packages/backend-php/vhs}/domain/DomainValueCache.php (55%) create mode 100644 packages/backend-php/vhs/domain/Filter.php create mode 100644 packages/backend-php/vhs/domain/IDomain.php create mode 100644 packages/backend-php/vhs/domain/ISchema.php rename {vhs => packages/backend-php/vhs}/domain/Notifier.php (67%) rename {vhs => packages/backend-php/vhs}/domain/Schema.php (68%) rename {vhs => packages/backend-php/vhs}/domain/collections/ChildDomainCollection.php (76%) create mode 100644 packages/backend-php/vhs/domain/collections/DomainCollection.php rename {vhs => packages/backend-php/vhs}/domain/collections/ParentDomainCollection.php (53%) rename {vhs => packages/backend-php/vhs}/domain/collections/SatelliteDomainCollection.php (77%) rename {vhs => packages/backend-php/vhs}/domain/exceptions/DomainException.php (90%) rename {vhs => packages/backend-php/vhs}/domain/exceptions/InvalidColumnDefinitionException.php (91%) rename {vhs => packages/backend-php/vhs}/domain/validations/ValidationException.php (70%) rename {vhs => packages/backend-php/vhs}/domain/validations/ValidationFailure.php (58%) rename {vhs => packages/backend-php/vhs}/domain/validations/ValidationResults.php (60%) create mode 100644 packages/backend-php/vhs/exceptions/HttpException.php create mode 100644 packages/backend-php/vhs/gateways/Engine.php create mode 100644 packages/backend-php/vhs/gateways/interfaces/IGateway.php create mode 100644 packages/backend-php/vhs/gateways/interfaces/IMessagesEmailGateway.php create mode 100644 packages/backend-php/vhs/gateways/messages/email/AWSSESClient.php rename {vhs => packages/backend-php/vhs}/loggers/ConsoleLogger.php (72%) rename {vhs => packages/backend-php/vhs}/loggers/FileLogger.php (59%) rename {vhs => packages/backend-php/vhs}/loggers/SilentLogger.php (67%) rename {vhs => packages/backend-php/vhs}/loggers/StringLogger.php (65%) rename {vhs => packages/backend-php/vhs}/migration/Backup.php (73%) rename {vhs => packages/backend-php/vhs}/migration/Migrator.php (75%) create mode 100644 packages/backend-php/vhs/monitors/Monitor.php rename {vhs => packages/backend-php/vhs}/security/AnonPrincipal.php (97%) rename {vhs => packages/backend-php/vhs}/security/BearerTokenCredentials.php (59%) rename {vhs => packages/backend-php/vhs}/security/CurrentUser.php (64%) rename {vhs => packages/backend-php/vhs}/security/IAuthenticate.php (62%) rename {vhs => packages/backend-php/vhs}/security/ICredentials.php (88%) create mode 100644 packages/backend-php/vhs/security/IPrincipal.php rename {vhs => packages/backend-php/vhs}/security/SystemPrincipal.php (96%) rename {vhs => packages/backend-php/vhs}/security/UserPassCredentials.php (55%) create mode 100644 packages/backend-php/vhs/security/exceptions/InvalidCredentials.php create mode 100644 packages/backend-php/vhs/security/exceptions/UnauthorizedException.php rename {vhs => packages/backend-php/vhs}/services/IContract.php (88%) create mode 100644 packages/backend-php/vhs/services/Service.php rename {vhs => packages/backend-php/vhs}/services/ServiceClient.php (74%) rename {vhs => packages/backend-php/vhs}/services/ServiceContext.php (62%) rename {vhs => packages/backend-php/vhs}/services/ServiceHandler.php (72%) rename {vhs => packages/backend-php/vhs}/services/ServiceRegistry.php (67%) rename {vhs => packages/backend-php/vhs}/services/endpoints/Endpoint.php (81%) create mode 100644 packages/backend-php/vhs/services/endpoints/IEndpoint.php rename {vhs => packages/backend-php/vhs}/services/endpoints/JsonEndpoint.php (54%) rename {vhs => packages/backend-php/vhs}/services/endpoints/NativeEndpoint.php (52%) rename {vhs => packages/backend-php/vhs}/services/exceptions/InvalidContractException.php (90%) create mode 100644 packages/backend-php/vhs/services/exceptions/InvalidRequestException.php rename {vhs => packages/backend-php/vhs}/vhs.php (100%) rename {vhs => packages/backend-php/vhs}/web/HttpContext.php (68%) rename {vhs => packages/backend-php/vhs}/web/HttpRequest.php (70%) rename {vhs => packages/backend-php/vhs}/web/HttpRequestHandler.php (62%) rename {vhs => packages/backend-php/vhs}/web/HttpServer.php (62%) rename {vhs => packages/backend-php/vhs}/web/HttpUtil.php (77%) create mode 100644 packages/backend-php/vhs/web/IHttpModule.php create mode 100644 packages/backend-php/vhs/web/enums/HttpStatusCodes.php rename {vhs => packages/backend-php/vhs}/web/modules/HttpBasicAuthModule.php (68%) rename {vhs => packages/backend-php/vhs}/web/modules/HttpBearerTokenAuthModule.php (69%) rename {vhs => packages/backend-php/vhs}/web/modules/HttpExceptionHandlerModule.php (52%) create mode 100644 packages/backend-php/vhs/web/modules/HttpJsonServiceHandlerModule.php rename {vhs => packages/backend-php/vhs}/web/modules/HttpRequestHandlerModule.php (58%) rename {vhs => packages/backend-php/vhs}/web/modules/HttpServerInfoModule.php (51%) create mode 100644 packages/frontend-react/.bowerrc create mode 100644 packages/frontend-react/.editorconfig create mode 100644 packages/frontend-react/.prettierignore create mode 100644 packages/frontend-react/.storybook/main.ts create mode 100644 packages/frontend-react/.storybook/preview-head.html create mode 100644 packages/frontend-react/.storybook/preview.tsx create mode 100644 packages/frontend-react/README.md create mode 100644 packages/frontend-react/bower.json create mode 100644 packages/frontend-react/conf/nginx-react-docker-compose.conf create mode 100644 packages/frontend-react/docs/Development.md create mode 100644 packages/frontend-react/eslint.config.mjs create mode 100644 packages/frontend-react/generate-react-cli.json create mode 100644 packages/frontend-react/index.html create mode 100644 packages/frontend-react/package.json create mode 100644 packages/frontend-react/postcss.config.js create mode 100644 packages/frontend-react/prettier.config.mjs create mode 100644 packages/frontend-react/public/apiMockServiceWorker.js rename {web => packages/frontend-react/public}/badges/cert_cnc_mill_lathe.svg (100%) rename {web => packages/frontend-react/public}/badges/cert_laser.svg (100%) rename {web => packages/frontend-react/public}/badges/door_access.svg (100%) rename {web => packages/frontend-react/public}/badges/key_holder.svg (100%) rename {web => packages/frontend-react/public}/badges/key_holder_inactive.svg (100%) rename {web => packages/frontend-react/public}/badges/laser.png (100%) rename {web => packages/frontend-react/public}/badges/newsletter.svg (100%) create mode 100644 packages/frontend-react/public/custom/pace.local.css rename {web => packages/frontend-react/public}/favicon.ico (100%) rename {web => packages/frontend-react/public}/images/logo.png (100%) create mode 100644 packages/frontend-react/public/images/logo.svg rename {web => packages/frontend-react/public}/images/provider/github.png (100%) rename {web => packages/frontend-react/public}/images/provider/google.png (100%) rename {web => packages/frontend-react/public}/images/provider/pin.png (100%) rename {web => packages/frontend-react/public}/images/provider/rfid.png (100%) rename {web => packages/frontend-react/public}/images/provider/slack.png (100%) rename {web => packages/frontend-react/public}/images/slack.png (100%) create mode 100644 packages/frontend-react/public/images/under-construction90s-90s.gif rename {web => packages/frontend-react/public}/images/vhs-member-card-2015-2-full.png (100%) rename {web => packages/frontend-react/public}/images/vhs-member-card-2015-2-thumb.png (100%) rename {web => packages/frontend-react/public}/images/vhs-member-card-2015-full.png (100%) rename {web => packages/frontend-react/public}/images/vhs-member-card-2015-thumb.png (100%) create mode 100644 packages/frontend-react/public/mockServiceWorker.js create mode 100644 packages/frontend-react/public/robots.txt create mode 100644 packages/frontend-react/skel/config.json create mode 100644 packages/frontend-react/src/components/01-atoms/Col/Col.lazy.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Col/Col.stories.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Col/Col.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Col/Col.types.ts rename web/user/home/home.html => packages/frontend-react/src/components/01-atoms/Col/Col.utils.ts (100%) create mode 100644 packages/frontend-react/src/components/01-atoms/Conditional/Conditional.stories.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Conditional/Conditional.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Conditional/Conditional.types.ts create mode 100644 packages/frontend-react/src/components/01-atoms/Conditional/Conditional.utils.ts create mode 100644 packages/frontend-react/src/components/01-atoms/Container/Container.lazy.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Container/Container.stories.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Container/Container.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Container/Container.types.ts create mode 100644 packages/frontend-react/src/components/01-atoms/Container/Container.utils.ts create mode 100644 packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.lazy.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.stories.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.types.ts create mode 100644 packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.utils.ts create mode 100644 packages/frontend-react/src/components/01-atoms/NavBar/NavBar.lazy.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/NavBar/NavBar.stories.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/NavBar/NavBar.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/NavBar/NavBar.types.ts create mode 100644 packages/frontend-react/src/components/01-atoms/NavBar/NavBar.utils.ts create mode 100644 packages/frontend-react/src/components/01-atoms/Overlay/Overlay.lazy.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Overlay/Overlay.stories.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Overlay/Overlay.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Overlay/Overlay.types.ts create mode 100644 packages/frontend-react/src/components/01-atoms/Overlay/Overlay.utils.ts create mode 100644 packages/frontend-react/src/components/01-atoms/Pill/Pill.lazy.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Pill/Pill.stories.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Pill/Pill.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Pill/Pill.types.ts create mode 100644 packages/frontend-react/src/components/01-atoms/Pill/Pill.utils.ts create mode 100644 packages/frontend-react/src/components/01-atoms/Popover/Popover.lazy.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Popover/Popover.stories.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Popover/Popover.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Popover/Popover.types.ts create mode 100644 packages/frontend-react/src/components/01-atoms/Popover/Popover.utils.ts create mode 100644 packages/frontend-react/src/components/01-atoms/Row/Row.lazy.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Row/Row.stories.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Row/Row.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/Row/Row.types.ts create mode 100644 packages/frontend-react/src/components/01-atoms/Row/Row.utils.ts create mode 100644 packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.lazy.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.stories.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.types.ts create mode 100644 packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.utils.ts create mode 100644 packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.lazy.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.settings.ts create mode 100644 packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.stories.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.tsx create mode 100644 packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.types.ts create mode 100644 packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.module.css create mode 100644 packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/Button/Button.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/Button/Button.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/Button/Button.styles.ts create mode 100644 packages/frontend-react/src/components/02-molecules/Button/Button.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/Button/Button.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/Button/Button.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.test.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/FormCol/FormCol.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/FormCol/FormCol.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/FormCol/FormCol.test.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/FormCol/FormCol.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/FormCol/FormCol.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/FormCol/FormCol.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/FormRow/FormRow.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/FormRow/FormRow.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/FormRow/FormRow.test.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/FormRow/FormRow.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/FormRow/FormRow.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/FormRow/FormRow.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.test.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/Loading/Loading.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/Loading/Loading.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/Loading/Loading.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/Loading/Loading.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.test.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.test.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.ui.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.test.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.test.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/Toggle/Toggle.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/Toggle/Toggle.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/Toggle/Toggle.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/Toggle/Toggle.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/Toggle/Toggle.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.utils.ts create mode 100644 packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.lazy.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.stories.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.tsx create mode 100644 packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.types.ts create mode 100644 packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/Card/Card.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Card/Card.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Card/Card.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.test.tsx create mode 100644 packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.tsx create mode 100644 packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControl.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControl.module.css create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControl.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControl.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControl.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControl.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.module.css create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.module.css create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.module.css create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.module.css create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.module.css create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.tsx create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/GridChart/GridChart.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/GridChart/GridChart.module.css create mode 100644 packages/frontend-react/src/components/03-particles/GridChart/GridChart.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/GridChart/GridChart.tsx create mode 100644 packages/frontend-react/src/components/03-particles/GridChart/GridChart.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/GridChart/GridChart.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/LoadingOverlay/LoadingOverlay.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/LoadingOverlay/LoadingOverlay.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/LoadingOverlay/LoadingOverlay.tsx create mode 100644 packages/frontend-react/src/components/03-particles/LoadingOverlay/LoadingOverlay.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/LoadingOverlay/LoadingOverlay.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/Menu/Menu.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Menu/Menu.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Menu/Menu.test.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Menu/Menu.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Menu/Menu.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/Menu/Menu.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/Menu/MenuItem/MenuItem.css create mode 100644 packages/frontend-react/src/components/03-particles/Menu/MenuItem/MenuItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Menu/MenuItem/MenuItem.module.css create mode 100644 packages/frontend-react/src/components/03-particles/Menu/MenuItem/MenuItem.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Menu/MenuItem/MenuItem.test.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Menu/MenuItem/MenuItem.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Menu/MenuItem/MenuItem.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/Menu/MenuItem/MenuItem.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/Modal/Modal.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/Modal.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalBody/ModalBody.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalBody/ModalBody.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalBody/ModalBody.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalBody/ModalBody.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalBody/ModalBody.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalContainer/ModalContainer.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalContainer/ModalContainer.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalContainer/ModalContainer.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalContainer/ModalContainer.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalContainer/ModalContainer.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalFooter/ModalFooter.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalFooter/ModalFooter.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalFooter/ModalFooter.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalFooter/ModalFooter.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalFooter/ModalFooter.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalHeader/ModalHeader.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalHeader/ModalHeader.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalHeader/ModalHeader.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalHeader/ModalHeader.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalHeader/ModalHeader.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalTitle/ModalTitle.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalTitle/ModalTitle.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalTitle/ModalTitle.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalTitle/ModalTitle.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/Modal/ModalTitle/ModalTitle.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/Modal/index.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Paginator/Paginator.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Paginator/Paginator.module.css create mode 100644 packages/frontend-react/src/components/03-particles/Paginator/Paginator.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Paginator/Paginator.tsx create mode 100644 packages/frontend-react/src/components/03-particles/Paginator/Paginator.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/Paginator/Paginator.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/PrivilegePill/PrivilegePill.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/PrivilegePill/PrivilegePill.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/PrivilegePill/PrivilegePill.test.tsx create mode 100644 packages/frontend-react/src/components/03-particles/PrivilegePill/PrivilegePill.tsx create mode 100644 packages/frontend-react/src/components/03-particles/PrivilegePill/PrivilegePill.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/PrivilegePill/PrivilegePill.utils.ts create mode 100644 packages/frontend-react/src/components/03-particles/TopBar/TopBar.lazy.tsx create mode 100644 packages/frontend-react/src/components/03-particles/TopBar/TopBar.stories.tsx create mode 100644 packages/frontend-react/src/components/03-particles/TopBar/TopBar.tsx create mode 100644 packages/frontend-react/src/components/03-particles/TopBar/TopBar.types.ts create mode 100644 packages/frontend-react/src/components/03-particles/TopBar/TopBar.utils.ts create mode 100644 packages/frontend-react/src/components/04-composites/AdminStatusWidget/AdminStatusWidget.lazy.tsx create mode 100644 packages/frontend-react/src/components/04-composites/AdminStatusWidget/AdminStatusWidget.stories.tsx create mode 100644 packages/frontend-react/src/components/04-composites/AdminStatusWidget/AdminStatusWidget.test.tsx create mode 100644 packages/frontend-react/src/components/04-composites/AdminStatusWidget/AdminStatusWidget.tsx create mode 100644 packages/frontend-react/src/components/04-composites/AdminStatusWidget/AdminStatusWidget.types.ts create mode 100644 packages/frontend-react/src/components/04-composites/AdminStatusWidget/AdminStatusWidget.utils.ts create mode 100644 packages/frontend-react/src/components/04-composites/BasePage/BasePage.lazy.tsx create mode 100644 packages/frontend-react/src/components/04-composites/BasePage/BasePage.module.css create mode 100644 packages/frontend-react/src/components/04-composites/BasePage/BasePage.stories.tsx create mode 100644 packages/frontend-react/src/components/04-composites/BasePage/BasePage.tsx create mode 100644 packages/frontend-react/src/components/04-composites/BasePage/BasePage.types.ts create mode 100644 packages/frontend-react/src/components/04-composites/BasePage/BasePage.utils.ts create mode 100644 packages/frontend-react/src/components/04-composites/CurrentUserPrivilegesCard/CurrentUserPrivilegesCard.lazy.tsx create mode 100644 packages/frontend-react/src/components/04-composites/CurrentUserPrivilegesCard/CurrentUserPrivilegesCard.mocks.ts create mode 100644 packages/frontend-react/src/components/04-composites/CurrentUserPrivilegesCard/CurrentUserPrivilegesCard.stories.tsx create mode 100644 packages/frontend-react/src/components/04-composites/CurrentUserPrivilegesCard/CurrentUserPrivilegesCard.test.tsx create mode 100644 packages/frontend-react/src/components/04-composites/CurrentUserPrivilegesCard/CurrentUserPrivilegesCard.tsx create mode 100644 packages/frontend-react/src/components/04-composites/CurrentUserPrivilegesCard/CurrentUserPrivilegesCard.types.ts create mode 100644 packages/frontend-react/src/components/04-composites/CurrentUserPrivilegesCard/CurrentUserPrivilegesCard.utils.ts create mode 100644 packages/frontend-react/src/components/04-composites/CurrentUserPrivilegesCard/CurrentUserPrivilegesList/CurrentUserPrivilegesList.lazy.tsx create mode 100644 packages/frontend-react/src/components/04-composites/CurrentUserPrivilegesCard/CurrentUserPrivilegesList/CurrentUserPrivilegesList.stories.tsx create mode 100644 packages/frontend-react/src/components/04-composites/CurrentUserPrivilegesCard/CurrentUserPrivilegesList/CurrentUserPrivilegesList.test.tsx create mode 100644 packages/frontend-react/src/components/04-composites/CurrentUserPrivilegesCard/CurrentUserPrivilegesList/CurrentUserPrivilegesList.tsx create mode 100644 packages/frontend-react/src/components/04-composites/CurrentUserPrivilegesCard/CurrentUserPrivilegesList/CurrentUserPrivilegesList.types.ts create mode 100644 packages/frontend-react/src/components/04-composites/CurrentUserPrivilegesCard/CurrentUserPrivilegesList/CurrentUserPrivilegesList.utils.ts create mode 100644 packages/frontend-react/src/components/04-composites/DomainSelectorCard/DomainSelectorCard.lazy.tsx create mode 100644 packages/frontend-react/src/components/04-composites/DomainSelectorCard/DomainSelectorCard.stories.tsx create mode 100644 packages/frontend-react/src/components/04-composites/DomainSelectorCard/DomainSelectorCard.test.tsx create mode 100644 packages/frontend-react/src/components/04-composites/DomainSelectorCard/DomainSelectorCard.tsx create mode 100644 packages/frontend-react/src/components/04-composites/DomainSelectorCard/DomainSelectorCard.types.ts create mode 100644 packages/frontend-react/src/components/04-composites/DomainSelectorCard/DomainSelectorCard.utils.ts create mode 100644 packages/frontend-react/src/components/04-composites/MembershipSelectorCard/MembershipSelectorCard.lazy.tsx create mode 100644 packages/frontend-react/src/components/04-composites/MembershipSelectorCard/MembershipSelectorCard.stories.tsx create mode 100644 packages/frontend-react/src/components/04-composites/MembershipSelectorCard/MembershipSelectorCard.test.tsx create mode 100644 packages/frontend-react/src/components/04-composites/MembershipSelectorCard/MembershipSelectorCard.tsx create mode 100644 packages/frontend-react/src/components/04-composites/MembershipSelectorCard/MembershipSelectorCard.types.ts create mode 100644 packages/frontend-react/src/components/04-composites/MembershipSelectorCard/MembershipSelectorCard.utils.ts create mode 100644 packages/frontend-react/src/components/04-composites/MobileMenu/MobileMenu.lazy.tsx create mode 100644 packages/frontend-react/src/components/04-composites/MobileMenu/MobileMenu.stories.tsx create mode 100644 packages/frontend-react/src/components/04-composites/MobileMenu/MobileMenu.test.tsx create mode 100644 packages/frontend-react/src/components/04-composites/MobileMenu/MobileMenu.tsx create mode 100644 packages/frontend-react/src/components/04-composites/MobileMenu/MobileMenu.types.ts create mode 100644 packages/frontend-react/src/components/04-composites/MobileMenu/MobileMenu.utils.ts create mode 100644 packages/frontend-react/src/components/04-composites/OverlayCard/OverlayCard.lazy.tsx create mode 100644 packages/frontend-react/src/components/04-composites/OverlayCard/OverlayCard.stories.tsx create mode 100644 packages/frontend-react/src/components/04-composites/OverlayCard/OverlayCard.test.tsx create mode 100644 packages/frontend-react/src/components/04-composites/OverlayCard/OverlayCard.tsx create mode 100644 packages/frontend-react/src/components/04-composites/OverlayCard/OverlayCard.types.ts create mode 100644 packages/frontend-react/src/components/04-composites/OverlayCard/OverlayCard.utils.ts create mode 100644 packages/frontend-react/src/components/04-composites/PrivilegesSelectorCard/PrivilegesSelectorCard.lazy.tsx create mode 100644 packages/frontend-react/src/components/04-composites/PrivilegesSelectorCard/PrivilegesSelectorCard.stories.tsx create mode 100644 packages/frontend-react/src/components/04-composites/PrivilegesSelectorCard/PrivilegesSelectorCard.test.tsx create mode 100644 packages/frontend-react/src/components/04-composites/PrivilegesSelectorCard/PrivilegesSelectorCard.tsx create mode 100644 packages/frontend-react/src/components/04-composites/PrivilegesSelectorCard/PrivilegesSelectorCard.types.ts create mode 100644 packages/frontend-react/src/components/04-composites/PrivilegesSelectorCard/PrivilegesSelectorCard.utils.ts create mode 100644 packages/frontend-react/src/components/04-composites/RootComponent/RootComponent.lazy.tsx create mode 100644 packages/frontend-react/src/components/04-composites/RootComponent/RootComponent.stories.tsx create mode 100644 packages/frontend-react/src/components/04-composites/RootComponent/RootComponent.test.tsx create mode 100644 packages/frontend-react/src/components/04-composites/RootComponent/RootComponent.tsx create mode 100644 packages/frontend-react/src/components/04-composites/RootComponent/RootComponent.types.ts create mode 100644 packages/frontend-react/src/components/04-composites/RootComponent/RootComponent.utils.ts create mode 100644 packages/frontend-react/src/components/04-composites/SelectorCard/SelectorCard.lazy.tsx create mode 100644 packages/frontend-react/src/components/04-composites/SelectorCard/SelectorCard.stories.tsx create mode 100644 packages/frontend-react/src/components/04-composites/SelectorCard/SelectorCard.test.tsx create mode 100644 packages/frontend-react/src/components/04-composites/SelectorCard/SelectorCard.tsx create mode 100644 packages/frontend-react/src/components/04-composites/SelectorCard/SelectorCard.types.ts create mode 100644 packages/frontend-react/src/components/04-composites/SelectorCard/SelectorCard.utils.ts create mode 100644 packages/frontend-react/src/components/04-composites/Tabs/Tab/Tab.lazy.tsx create mode 100644 packages/frontend-react/src/components/04-composites/Tabs/Tab/Tab.stories.tsx create mode 100644 packages/frontend-react/src/components/04-composites/Tabs/Tab/Tab.test.tsx create mode 100644 packages/frontend-react/src/components/04-composites/Tabs/Tab/Tab.tsx create mode 100644 packages/frontend-react/src/components/04-composites/Tabs/Tab/Tab.types.ts create mode 100644 packages/frontend-react/src/components/04-composites/Tabs/Tab/Tab.utils.ts create mode 100644 packages/frontend-react/src/components/04-composites/Tabs/Tabs.lazy.tsx create mode 100644 packages/frontend-react/src/components/04-composites/Tabs/Tabs.stories.tsx create mode 100644 packages/frontend-react/src/components/04-composites/Tabs/Tabs.test.tsx create mode 100644 packages/frontend-react/src/components/04-composites/Tabs/Tabs.tsx create mode 100644 packages/frontend-react/src/components/04-composites/Tabs/Tabs.types.ts create mode 100644 packages/frontend-react/src/components/04-composites/Tabs/Tabs.utils.ts create mode 100644 packages/frontend-react/src/components/04-composites/WaitingRoom/WaitingRoom.lazy.tsx create mode 100644 packages/frontend-react/src/components/04-composites/WaitingRoom/WaitingRoom.stories.tsx create mode 100644 packages/frontend-react/src/components/04-composites/WaitingRoom/WaitingRoom.tsx create mode 100644 packages/frontend-react/src/components/04-composites/WaitingRoom/WaitingRoom.types.ts create mode 100644 packages/frontend-react/src/components/04-composites/WaitingRoom/WaitingRoom.utils.ts create mode 100644 packages/frontend-react/src/components/05-materials/InfoButton/InfoButton.lazy.tsx create mode 100644 packages/frontend-react/src/components/05-materials/InfoButton/InfoButton.stories.tsx create mode 100644 packages/frontend-react/src/components/05-materials/InfoButton/InfoButton.test.tsx create mode 100644 packages/frontend-react/src/components/05-materials/InfoButton/InfoButton.tsx create mode 100644 packages/frontend-react/src/components/05-materials/InfoButton/InfoButton.types.ts create mode 100644 packages/frontend-react/src/components/05-materials/InfoButton/InfoButton.utils.ts create mode 100644 packages/frontend-react/src/components/05-materials/ItemDeleteModal/ItemDeleteModal.lazy.tsx create mode 100644 packages/frontend-react/src/components/05-materials/ItemDeleteModal/ItemDeleteModal.stories.tsx create mode 100644 packages/frontend-react/src/components/05-materials/ItemDeleteModal/ItemDeleteModal.test.tsx create mode 100644 packages/frontend-react/src/components/05-materials/ItemDeleteModal/ItemDeleteModal.tsx create mode 100644 packages/frontend-react/src/components/05-materials/ItemDeleteModal/ItemDeleteModal.types.ts create mode 100644 packages/frontend-react/src/components/05-materials/ItemDeleteModal/ItemDeleteModal.utils.ts create mode 100644 packages/frontend-react/src/components/05-materials/TablePage/TablePage.context.ts create mode 100644 packages/frontend-react/src/components/05-materials/TablePage/TablePage.lazy.tsx create mode 100644 packages/frontend-react/src/components/05-materials/TablePage/TablePage.schema.ts create mode 100644 packages/frontend-react/src/components/05-materials/TablePage/TablePage.stories.tsx create mode 100644 packages/frontend-react/src/components/05-materials/TablePage/TablePage.test.tsx create mode 100644 packages/frontend-react/src/components/05-materials/TablePage/TablePage.tsx create mode 100644 packages/frontend-react/src/components/05-materials/TablePage/TablePage.types.ts create mode 100644 packages/frontend-react/src/components/05-materials/TablePage/TablePage.utils.ts create mode 100644 packages/frontend-react/src/components/06-layouts/AdminLayout/AdminLayout.lazy.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/AdminLayout/AdminLayout.stories.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/AdminLayout/AdminLayout.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/AdminLayout/AdminLayout.types.ts create mode 100644 packages/frontend-react/src/components/06-layouts/AdminLayout/AdminLayout.ui.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/AdminLayout/AdminLayout.utils.ts create mode 100644 packages/frontend-react/src/components/06-layouts/AuthenticatedLayout/AuthenticatedLayout.lazy.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/AuthenticatedLayout/AuthenticatedLayout.stories.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/AuthenticatedLayout/AuthenticatedLayout.test.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/AuthenticatedLayout/AuthenticatedLayout.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/AuthenticatedLayout/AuthenticatedLayout.types.ts create mode 100644 packages/frontend-react/src/components/06-layouts/AuthenticatedLayout/AuthenticatedLayout.utils.ts create mode 100644 packages/frontend-react/src/components/06-layouts/MainLayout/MainLayout.lazy.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/MainLayout/MainLayout.stories.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/MainLayout/MainLayout.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/MainLayout/MainLayout.types.ts create mode 100644 packages/frontend-react/src/components/06-layouts/MainLayout/MainLayout.utils.ts create mode 100644 packages/frontend-react/src/components/06-layouts/UserLayout/UserLayout.lazy.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/UserLayout/UserLayout.stories.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/UserLayout/UserLayout.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/UserLayout/UserLayout.types.ts create mode 100644 packages/frontend-react/src/components/06-layouts/UserLayout/UserLayout.ui.tsx create mode 100644 packages/frontend-react/src/components/06-layouts/UserLayout/UserLayout.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysCreateNewButton/ApiKeysCreateNewButton.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysCreateNewButton/ApiKeysCreateNewButton.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysCreateNewButton/ApiKeysCreateNewButton.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysCreateNewButton/ApiKeysCreateNewButton.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysCreateNewButton/ApiKeysCreateNewButton.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysEditModal/ApiKeysEditModal.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysEditModal/ApiKeysEditModal.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysEditModal/ApiKeysEditModal.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysEditModal/ApiKeysEditModal.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysEditModal/ApiKeysEditModal.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysHelpPage/ApiKeysHelpPage.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysHelpPage/ApiKeysHelpPage.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysHelpPage/ApiKeysHelpPage.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysHelpPage/ApiKeysHelpPage.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysHelpPage/ApiKeysHelpPage.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysListPage/ApiKeysListItem/ApiKeysListItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysListPage/ApiKeysListItem/ApiKeysListItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysListPage/ApiKeysListItem/ApiKeysListItem.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysListPage/ApiKeysListItem/ApiKeysListItem.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysListPage/ApiKeysListItem/ApiKeysListItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysListPage/ApiKeysListPage.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysListPage/ApiKeysListPage.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysListPage/ApiKeysListPage.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysListPage/ApiKeysListPage.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysListPage/ApiKeysListPage.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysNewModal/ApiKeysNewModal.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysNewModal/ApiKeysNewModal.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysNewModal/ApiKeysNewModal.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysNewModal/ApiKeysNewModal.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysNewModal/ApiKeysNewModal.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPage.context.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPage.guard.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPage.hooks.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPage.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPage.module.css create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPage.schemas.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPage.settings.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPage.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPage.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPage.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPage.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPageContainer/ApiKeysPageContainer.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPageContainer/ApiKeysPageContainer.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPageContainer/ApiKeysPageContainer.test.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPageContainer/ApiKeysPageContainer.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPageContainer/ApiKeysPageContainer.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysPageContainer/ApiKeysPageContainer.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysUsagePage/ApiKeysUsagePage.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysUsagePage/ApiKeysUsagePage.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysUsagePage/ApiKeysUsagePage.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysUsagePage/ApiKeysUsagePage.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/ApiKeysPage/ApiKeysUsagePage/ApiKeysUsagePage.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPage.context.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPage.guards.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPage.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPage.schemas.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPage.settings.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPage.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPage.test.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPage.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPage.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPage.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageClientView/OAuthPageClientItem/OAuthPageClientItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageClientView/OAuthPageClientItem/OAuthPageClientItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageClientView/OAuthPageClientItem/OAuthPageClientItem.test.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageClientView/OAuthPageClientItem/OAuthPageClientItem.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageClientView/OAuthPageClientItem/OAuthPageClientItem.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageClientView/OAuthPageClientItem/OAuthPageClientItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageClientView/OAuthPageClientView.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageClientView/OAuthPageClientView.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageClientView/OAuthPageClientView.test.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageClientView/OAuthPageClientView.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageClientView/OAuthPageClientView.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageClientView/OAuthPageClientView.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageContainer/OAuthPageContainer.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageContainer/OAuthPageContainer.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageContainer/OAuthPageContainer.test.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageContainer/OAuthPageContainer.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageContainer/OAuthPageContainer.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageContainer/OAuthPageContainer.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageDefaultView/OAuthPageDefaultView.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageDefaultView/OAuthPageDefaultView.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageDefaultView/OAuthPageDefaultView.test.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageDefaultView/OAuthPageDefaultView.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageDefaultView/OAuthPageDefaultView.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageDefaultView/OAuthPageDefaultView.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageEditClientModal/OAuthPageEditClientModal.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageEditClientModal/OAuthPageEditClientModal.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageEditClientModal/OAuthPageEditClientModal.test.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageEditClientModal/OAuthPageEditClientModal.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageEditClientModal/OAuthPageEditClientModal.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageEditClientModal/OAuthPageEditClientModal.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageNewClientButton/OAuthPageNewClientButton.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageNewClientButton/OAuthPageNewClientButton.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageNewClientButton/OAuthPageNewClientButton.test.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageNewClientButton/OAuthPageNewClientButton.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageNewClientButton/OAuthPageNewClientButton.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageNewClientButton/OAuthPageNewClientButton.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageNewClientModal/OAuthPageNewClientModal.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageNewClientModal/OAuthPageNewClientModal.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageNewClientModal/OAuthPageNewClientModal.test.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageNewClientModal/OAuthPageNewClientModal.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageNewClientModal/OAuthPageNewClientModal.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/OAuthPage/OAuthPageNewClientModal/OAuthPageNewClientModal.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/CreateWebHookForm/CreateWebHookForm.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/CreateWebHookForm/CreateWebHookForm.schema.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/CreateWebHookForm/CreateWebHookForm.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/CreateWebHookForm/CreateWebHookForm.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/CreateWebHookForm/CreateWebHookForm.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/CreateWebHookForm/CreateWebHookForm.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/WebHooksItem/WebHooksItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/WebHooksItem/WebHooksItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/WebHooksItem/WebHooksItem.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/WebHooksItem/WebHooksItem.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/WebHooksItem/WebHooksItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/WebHooksPage.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/WebHooksPage.stories.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/WebHooksPage.test.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/WebHooksPage.tsx create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/WebHooksPage.types.ts create mode 100644 packages/frontend-react/src/components/07-integrated-pages/WebHooksPage/WebHooksPage.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/AccessLogs/AccessLogs.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/AccessLogs/AccessLogs.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/AccessLogs/AccessLogs.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/AccessLogs/AccessLogs.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/AccessLogs/AccessLogs.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/AccessLogs/AccessLogsItem/AccessLogsItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/AccessLogs/AccessLogsItem/AccessLogsItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/AccessLogs/AccessLogsItem/AccessLogsItem.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/AccessLogs/AccessLogsItem/AccessLogsItem.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/AccessLogs/AccessLogsItem/AccessLogsItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/ApiKeys/ApiKeys.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/ApiKeys/ApiKeys.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/ApiKeys/ApiKeys.test.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/ApiKeys/ApiKeys.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/ApiKeys/ApiKeys.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/ApiKeys/ApiKeys.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Dashboard/Dashboard.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Dashboard/Dashboard.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Dashboard/Dashboard.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Dashboard/Dashboard.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Dashboard/Dashboard.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/DatabaseBackup/DatabaseBackup.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/DatabaseBackup/DatabaseBackup.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/DatabaseBackup/DatabaseBackup.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/DatabaseBackup/DatabaseBackup.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/DatabaseBackup/DatabaseBackup.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateEdit/EmailTemplateEdit.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateEdit/EmailTemplateEdit.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateEdit/EmailTemplateEdit.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateEdit/EmailTemplateEdit.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateEdit/EmailTemplateEdit.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateItem/EmailTemplateItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateItem/EmailTemplateItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateItem/EmailTemplateItem.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateItem/EmailTemplateItem.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateItem/EmailTemplateItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateNew/EmailTemplateNew.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateNew/EmailTemplateNew.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateNew/EmailTemplateNew.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateNew/EmailTemplateNew.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplateNew/EmailTemplateNew.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplates.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplates.schema.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplates.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplates.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplates.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/EmailTemplates/EmailTemplates.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Events/Events.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Events/Events.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Events/Events.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Events/Events.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Events/Events.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Events/EventsItem/EventsItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Events/EventsItem/EventsItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Events/EventsItem/EventsItem.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Events/EventsItem/EventsItem.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Events/EventsItem/EventsItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/IPNRecords/IPNRecords.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/IPNRecords/IPNRecords.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/IPNRecords/IPNRecords.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/IPNRecords/IPNRecords.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/IPNRecords/IPNRecords.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/IPNRecords/IPNRecordsItem/IPNRecordsItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/IPNRecords/IPNRecordsItem/IPNRecordsItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/IPNRecords/IPNRecordsItem/IPNRecordsItem.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/IPNRecords/IPNRecordsItem/IPNRecordsItem.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/IPNRecords/IPNRecordsItem/IPNRecordsItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Logs/Logs.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Logs/Logs.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Logs/Logs.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Logs/Logs.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Logs/Logs.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/IssueGenuineCard/IssueGenuineCard.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/IssueGenuineCard/IssueGenuineCard.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/IssueGenuineCard/IssueGenuineCard.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/IssueGenuineCard/IssueGenuineCard.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/IssueGenuineCard/IssueGenuineCard.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCardPurchases/ListGenuineCardPurchases.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCardPurchases/ListGenuineCardPurchases.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCardPurchases/ListGenuineCardPurchases.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCardPurchases/ListGenuineCardPurchases.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCardPurchases/ListGenuineCardPurchases.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCardPurchases/ListGenuineCardPurchasesItem/ListGenuineCardPurchasesItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCardPurchases/ListGenuineCardPurchasesItem/ListGenuineCardPurchasesItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCardPurchases/ListGenuineCardPurchasesItem/ListGenuineCardPurchasesItem.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCardPurchases/ListGenuineCardPurchasesItem/ListGenuineCardPurchasesItem.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCardPurchases/ListGenuineCardPurchasesItem/ListGenuineCardPurchasesItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCards/ListGenuineCards.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCards/ListGenuineCards.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCards/ListGenuineCards.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCards/ListGenuineCards.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCards/ListGenuineCards.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCards/ListGenuineCardsItem/ListGenuineCardsItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCards/ListGenuineCardsItem/ListGenuineCardsItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCards/ListGenuineCardsItem/ListGenuineCardsItem.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCards/ListGenuineCardsItem/ListGenuineCardsItem.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/ListGenuineCards/ListGenuineCardsItem/ListGenuineCardsItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/MemberCards.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/MemberCards.schema.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/MemberCards.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/MemberCards.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/MemberCards.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/MemberCards.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/RegisterGenuineCard/RegisterGenuineCard.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/RegisterGenuineCard/RegisterGenuineCard.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/RegisterGenuineCard/RegisterGenuineCard.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/RegisterGenuineCard/RegisterGenuineCard.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/MemberCards/RegisterGenuineCard/RegisterGenuineCard.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/Memberships.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/Memberships.schema.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/Memberships.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/Memberships.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/Memberships.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/Memberships.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/MembershipsEdit/MembershipsEdit.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/MembershipsEdit/MembershipsEdit.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/MembershipsEdit/MembershipsEdit.test.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/MembershipsEdit/MembershipsEdit.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/MembershipsEdit/MembershipsEdit.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/MembershipsEdit/MembershipsEdit.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/MembershipsItem/MembershipsItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/MembershipsItem/MembershipsItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/MembershipsItem/MembershipsItem.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/MembershipsItem/MembershipsItem.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Memberships/MembershipsItem/MembershipsItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Newsletter/Newsletter.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Newsletter/Newsletter.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Newsletter/Newsletter.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Newsletter/Newsletter.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Newsletter/Newsletter.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/OAuth/OAuth.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/OAuth/OAuth.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/OAuth/OAuth.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/OAuth/OAuth.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/OAuth/OAuth.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/PaymentGateways/PaymentGateways.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/PaymentGateways/PaymentGateways.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/PaymentGateways/PaymentGateways.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/PaymentGateways/PaymentGateways.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/PaymentGateways/PaymentGateways.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Payments/PayPalPaymentItem/PayPalPaymentItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Payments/PayPalPaymentItem/PayPalPaymentItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Payments/PayPalPaymentItem/PayPalPaymentItem.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Payments/PayPalPaymentItem/PayPalPaymentItem.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Payments/PayPalPaymentItem/PayPalPaymentItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Payments/Payments.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Payments/Payments.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Payments/Payments.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Payments/Payments.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Payments/Payments.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/Privileges.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/Privileges.schema.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/Privileges.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/Privileges.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/Privileges.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/Privileges.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/PrivilegesEdit/PrivilegesEdit.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/PrivilegesEdit/PrivilegesEdit.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/PrivilegesEdit/PrivilegesEdit.test.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/PrivilegesEdit/PrivilegesEdit.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/PrivilegesEdit/PrivilegesEdit.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/PrivilegesEdit/PrivilegesEdit.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/PrivilegesItem/PrivilegesItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/PrivilegesItem/PrivilegesItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/PrivilegesItem/PrivilegesItem.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/PrivilegesItem/PrivilegesItem.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Privileges/PrivilegesItem/PrivilegesItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Reports/Reports.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Reports/Reports.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Reports/Reports.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Reports/Reports.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Reports/Reports.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/SiteConfiguration/SiteConfiguration.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SiteConfiguration/SiteConfiguration.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SiteConfiguration/SiteConfiguration.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SiteConfiguration/SiteConfiguration.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/SiteConfiguration/SiteConfiguration.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/StripeRecords/StripeRecords.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/StripeRecords/StripeRecords.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/StripeRecords/StripeRecords.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/StripeRecords/StripeRecords.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/StripeRecords/StripeRecords.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/StripeRecords/StripeRecordsItem/StripeRecordsItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/StripeRecords/StripeRecordsItem/StripeRecordsItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/StripeRecords/StripeRecordsItem/StripeRecordsItem.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/StripeRecords/StripeRecordsItem/StripeRecordsItem.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/StripeRecords/StripeRecordsItem/StripeRecordsItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/CreateSystemPreferenceButton/CreateSystemPreferenceButton.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/CreateSystemPreferenceButton/CreateSystemPreferenceButton.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/CreateSystemPreferenceButton/CreateSystemPreferenceButton.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/CreateSystemPreferenceButton/CreateSystemPreferenceButton.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/CreateSystemPreferenceButton/CreateSystemPreferenceButton.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferences.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferences.schema.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferences.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferences.test.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferences.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferences.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferences.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesEdit/SystemPreferencesEdit.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesEdit/SystemPreferencesEdit.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesEdit/SystemPreferencesEdit.test.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesEdit/SystemPreferencesEdit.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesEdit/SystemPreferencesEdit.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesEdit/SystemPreferencesEdit.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesItem/SystemPreferencesItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesItem/SystemPreferencesItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesItem/SystemPreferencesItem.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesItem/SystemPreferencesItem.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesItem/SystemPreferencesItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesNew/SystemPreferencesNew.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesNew/SystemPreferencesNew.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesNew/SystemPreferencesNew.test.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesNew/SystemPreferencesNew.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesNew/SystemPreferencesNew.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/SystemPreferences/SystemPreferencesNew/SystemPreferencesNew.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/CreateUserButton/CreateUserButton.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/CreateUserButton/CreateUserButton.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/CreateUserButton/CreateUserButton.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/CreateUserButton/CreateUserButton.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/CreateUserButton/CreateUserButton.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/Users.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/Users.schema.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/Users.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/Users.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/Users.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/Users.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersEdit/UsersEdit.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersEdit/UsersEdit.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersEdit/UsersEdit.test.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersEdit/UsersEdit.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersEdit/UsersEdit.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersEdit/UsersEdit.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersItem/UsersItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersItem/UsersItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersItem/UsersItem.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersItem/UsersItem.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersItem/UsersItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersNew/UsersNew.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersNew/UsersNew.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersNew/UsersNew.test.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersNew/UsersNew.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersNew/UsersNew.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/Users/UsersNew/UsersNew.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/WebHooks/WebHooks.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/WebHooks/WebHooks.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/WebHooks/WebHooks.tsx create mode 100644 packages/frontend-react/src/components/07-pages/admin/WebHooks/WebHooks.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/admin/WebHooks/WebHooks.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/common/LandingPage/LandingPage.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/common/LandingPage/LandingPage.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/common/LandingPage/LandingPage.tsx create mode 100644 packages/frontend-react/src/components/07-pages/common/LandingPage/LandingPage.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/common/LandingPage/LandingPage.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/common/Login/Login.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/common/Login/Login.schema.ts create mode 100644 packages/frontend-react/src/components/07-pages/common/Login/Login.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/common/Login/Login.test.tsx create mode 100644 packages/frontend-react/src/components/07-pages/common/Login/Login.tsx create mode 100644 packages/frontend-react/src/components/07-pages/common/Login/Login.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/common/Login/Login.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/AccessHistory/AccessHistory.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/AccessHistory/AccessHistory.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/AccessHistory/AccessHistory.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/AccessHistory/AccessHistory.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/AccessHistory/AccessHistory.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/AccessHistory/AccessHistoryItem/AccessHistoryItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/AccessHistory/AccessHistoryItem/AccessHistoryItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/AccessHistory/AccessHistoryItem/AccessHistoryItem.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/AccessHistory/AccessHistoryItem/AccessHistoryItem.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/AccessHistory/AccessHistoryItem/AccessHistoryItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/ApiKeys/ApiKeys.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/ApiKeys/ApiKeys.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/ApiKeys/ApiKeys.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/ApiKeys/ApiKeys.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/ApiKeys/ApiKeys.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Dashboard/Dashboard.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Dashboard/Dashboard.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Dashboard/Dashboard.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Dashboard/Dashboard.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Dashboard/Dashboard.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/DoorAccess/DoorAccess.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/DoorAccess/DoorAccess.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/DoorAccess/DoorAccess.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/DoorAccess/DoorAccess.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/DoorAccess/DoorAccess.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/GetInvolved/GetInvolved.guard.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/GetInvolved/GetInvolved.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/GetInvolved/GetInvolved.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/GetInvolved/GetInvolved.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/GetInvolved/GetInvolved.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/GetInvolved/GetInvolved.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Granting/Granting.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Granting/Granting.schema.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Granting/Granting.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Granting/Granting.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Granting/Granting.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Granting/Granting.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Granting/GrantingItem/GrantingItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Granting/GrantingItem/GrantingItem.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Granting/GrantingItem/GrantingItem.test.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Granting/GrantingItem/GrantingItem.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Granting/GrantingItem/GrantingItem.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Granting/GrantingItem/GrantingItem.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Home/Home.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Home/Home.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Home/Home.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Home/Home.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Profile/APIKeysCard.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Profile/LinkedAccountsCard.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Profile/PinCard.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Profile/Profile.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Profile/Profile.schema.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Profile/Profile.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Profile/Profile.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Profile/Profile.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Profile/Profile.ui.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Profile/Profile.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Profile/RFIDKeysCard.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Profile/StandingCard.module.css create mode 100644 packages/frontend-react/src/components/07-pages/user/Profile/StandingCard.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Purchases/Purchases.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Purchases/Purchases.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Purchases/Purchases.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Purchases/Purchases.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Purchases/Purchases.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Transactions/TransactionItems/TransactionItems.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Transactions/TransactionItems/TransactionItems.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Transactions/TransactionItems/TransactionItems.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Transactions/TransactionItems/TransactionItems.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Transactions/TransactionItems/TransactionItems.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Transactions/Transactions.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Transactions/Transactions.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Transactions/Transactions.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/Transactions/Transactions.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/Transactions/Transactions.utils.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/WebHooks/WebHooks.lazy.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/WebHooks/WebHooks.stories.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/WebHooks/WebHooks.tsx create mode 100644 packages/frontend-react/src/components/07-pages/user/WebHooks/WebHooks.types.ts create mode 100644 packages/frontend-react/src/components/07-pages/user/WebHooks/WebHooks.utils.ts create mode 100644 packages/frontend-react/src/components/08-app/App/App.lazy.tsx create mode 100644 packages/frontend-react/src/components/08-app/App/App.react-router.tsx create mode 100644 packages/frontend-react/src/components/08-app/App/App.stories.tsx create mode 100644 packages/frontend-react/src/components/08-app/App/App.tsx create mode 100644 packages/frontend-react/src/components/08-app/App/App.types.ts create mode 100644 packages/frontend-react/src/components/08-app/App/App.utils.ts create mode 100644 packages/frontend-react/src/components/09-providers/AuthenticationProvider/AuthenticationProvider.context.ts create mode 100644 packages/frontend-react/src/components/09-providers/AuthenticationProvider/AuthenticationProvider.lazy.tsx create mode 100644 packages/frontend-react/src/components/09-providers/AuthenticationProvider/AuthenticationProvider.settings.ts create mode 100644 packages/frontend-react/src/components/09-providers/AuthenticationProvider/AuthenticationProvider.stories.tsx create mode 100644 packages/frontend-react/src/components/09-providers/AuthenticationProvider/AuthenticationProvider.tsx create mode 100644 packages/frontend-react/src/components/09-providers/AuthenticationProvider/AuthenticationProvider.types.ts create mode 100644 packages/frontend-react/src/components/09-providers/AuthenticationProvider/AuthenticationProvider.utils.ts create mode 100644 packages/frontend-react/src/components/09-providers/ConfigProvider/ConfigProvider.context.ts create mode 100644 packages/frontend-react/src/components/09-providers/ConfigProvider/ConfigProvider.guards.ts create mode 100644 packages/frontend-react/src/components/09-providers/ConfigProvider/ConfigProvider.hook.tsx create mode 100644 packages/frontend-react/src/components/09-providers/ConfigProvider/ConfigProvider.lazy.tsx create mode 100644 packages/frontend-react/src/components/09-providers/ConfigProvider/ConfigProvider.schemas.ts create mode 100644 packages/frontend-react/src/components/09-providers/ConfigProvider/ConfigProvider.stories.tsx create mode 100644 packages/frontend-react/src/components/09-providers/ConfigProvider/ConfigProvider.test.tsx create mode 100644 packages/frontend-react/src/components/09-providers/ConfigProvider/ConfigProvider.tsx create mode 100644 packages/frontend-react/src/components/09-providers/ConfigProvider/ConfigProvider.types.ts create mode 100644 packages/frontend-react/src/components/09-providers/ConfigProvider/ConfigProvider.utils.ts create mode 100644 packages/frontend-react/src/components/99-templates/admin-page/AdminTemplateName.lazy.tsx create mode 100644 packages/frontend-react/src/components/99-templates/admin-page/AdminTemplateName.stories.tsx create mode 100644 packages/frontend-react/src/components/99-templates/admin-page/AdminTemplateName.test.tsx create mode 100644 packages/frontend-react/src/components/99-templates/admin-page/AdminTemplateName.tsx create mode 100644 packages/frontend-react/src/components/99-templates/admin-page/AdminTemplateName.types.ts create mode 100644 packages/frontend-react/src/components/99-templates/admin-page/AdminTemplateName.utils.ts create mode 100644 packages/frontend-react/src/components/99-templates/admin-page/item/AdminTemplateNameItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/99-templates/admin-page/item/AdminTemplateNameItem.stories.tsx create mode 100644 packages/frontend-react/src/components/99-templates/admin-page/item/AdminTemplateNameItem.tsx create mode 100644 packages/frontend-react/src/components/99-templates/admin-page/item/AdminTemplateNameItem.types.ts create mode 100644 packages/frontend-react/src/components/99-templates/admin-page/item/AdminTemplateNameItem.utils.ts create mode 100644 packages/frontend-react/src/components/99-templates/default/TemplateName.lazy.tsx create mode 100644 packages/frontend-react/src/components/99-templates/default/TemplateName.stories.tsx create mode 100644 packages/frontend-react/src/components/99-templates/default/TemplateName.test.tsx create mode 100644 packages/frontend-react/src/components/99-templates/default/TemplateName.tsx create mode 100644 packages/frontend-react/src/components/99-templates/default/TemplateName.types.ts create mode 100644 packages/frontend-react/src/components/99-templates/default/TemplateName.utils.ts create mode 100644 packages/frontend-react/src/components/99-templates/page/TemplateName.lazy.tsx create mode 100644 packages/frontend-react/src/components/99-templates/page/TemplateName.stories.tsx create mode 100644 packages/frontend-react/src/components/99-templates/page/TemplateName.test.tsx create mode 100644 packages/frontend-react/src/components/99-templates/page/TemplateName.tsx create mode 100644 packages/frontend-react/src/components/99-templates/page/TemplateName.types.ts create mode 100644 packages/frontend-react/src/components/99-templates/page/TemplateName.utils.ts create mode 100644 packages/frontend-react/src/components/99-templates/user-page/TemplateName.lazy.tsx create mode 100644 packages/frontend-react/src/components/99-templates/user-page/TemplateName.stories.tsx create mode 100644 packages/frontend-react/src/components/99-templates/user-page/TemplateName.test.tsx create mode 100644 packages/frontend-react/src/components/99-templates/user-page/TemplateName.tsx create mode 100644 packages/frontend-react/src/components/99-templates/user-page/TemplateName.types.ts create mode 100644 packages/frontend-react/src/components/99-templates/user-page/TemplateName.utils.ts create mode 100644 packages/frontend-react/src/components/99-templates/user-page/UserTemplateNameItem/UserTemplateNameItem.lazy.tsx create mode 100644 packages/frontend-react/src/components/99-templates/user-page/UserTemplateNameItem/UserTemplateNameItem.stories.tsx create mode 100644 packages/frontend-react/src/components/99-templates/user-page/UserTemplateNameItem/UserTemplateNameItem.tsx create mode 100644 packages/frontend-react/src/components/99-templates/user-page/UserTemplateNameItem/UserTemplateNameItem.types.ts create mode 100644 packages/frontend-react/src/components/99-templates/user-page/UserTemplateNameItem/UserTemplateNameItem.utils.ts create mode 100644 packages/frontend-react/src/lib/backend.ts create mode 100644 packages/frontend-react/src/lib/db/models/PrincipalUser.ts create mode 100644 packages/frontend-react/src/lib/db/models/User.ts create mode 100644 packages/frontend-react/src/lib/db/utils/query-filters.ts create mode 100644 packages/frontend-react/src/lib/exceptions/HTTPException.ts create mode 100644 packages/frontend-react/src/lib/fetcher.ts create mode 100644 packages/frontend-react/src/lib/guards/common.ts create mode 100644 packages/frontend-react/src/lib/guards/records.ts create mode 100644 packages/frontend-react/src/lib/hooks/providers/ApiKeyService2/useDeleteApiKey.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/ApiKeyService2/useGenerateSystemApiKey.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/ApiKeyService2/useGenerateUserApiKey.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/ApiKeyService2/useGetApiKey.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/ApiKeyService2/useGetSystemApiKeys.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/ApiKeyService2/useGetUserApiKeys.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/ApiKeyService2/usePutApiKeyPrivileges.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/ApiKeyService2/useUpdateApiKey.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/AuthService2/useCheckPin.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/AuthService2/useCheckRfid.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/AuthService2/useCheckService.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/AuthService2/useCheckUsername.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/AuthService2/useCountAccessLog.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/AuthService2/useCountUserAccessLog.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/AuthService2/useCurrentUser.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/AuthService2/useGetUser.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/AuthService2/useListAccessLog.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/AuthService2/useListUserAccessLog.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/AuthService2/useLogin.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/AuthService2/useLogout.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/AuthService2/usePinLogin.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/AuthService2/useRfidLogin.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/CurrentUser/useGetCurrentUser.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/CurrentUser2/useGetCurrentUser.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EmailService2/useCountTemplates.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EmailService2/useDeleteTemplate.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EmailService2/useEmail.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EmailService2/useEmailUser.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EmailService2/useGetTemplate.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EmailService2/useListTemplates.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EmailService2/usePutTemplate.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EmailService2/useUpdateTemplate.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EmailService2/useUpdateTemplateBody.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EmailService2/useUpdateTemplateCode.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EmailService2/useUpdateTemplateHelp.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EmailService2/useUpdateTemplateHtml.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EmailService2/useUpdateTemplateName.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EmailService2/useUpdateTemplateSubject.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EventService2/useCountEvents.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EventService2/useCreateEvent.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EventService2/useDeleteEvent.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EventService2/useEnableEvent.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EventService2/useGetAccessibleEvents.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EventService2/useGetDomainDefinition.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EventService2/useGetDomainDefinitions.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EventService2/useGetEvent.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EventService2/useGetEventTypes.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EventService2/useGetEvents.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EventService2/useListEvents.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EventService2/usePutEventPrivileges.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/EventService2/useUpdateEvent.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/IpnService2/useCountRecords.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/IpnService2/useGet.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/IpnService2/useGetAll.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/IpnService2/useListRecords.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/KeyService2/useDeleteKey.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/KeyService2/useGenerateUserKey.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/KeyService2/useGetAllKeys.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/KeyService2/useGetKey.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/KeyService2/useGetSystemKeys.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/KeyService2/useGetUserKeys.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/KeyService2/usePutKeyPrivileges.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/KeyService2/useUpdateKey.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MemberCardService2/useCountGenuineCards.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MemberCardService2/useCountUserGenuineCards.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MemberCardService2/useGetGenuineCardDetails.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MemberCardService2/useIssueCard.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MemberCardService2/useListGenuineCards.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MemberCardService2/useListUserGenuineCards.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MemberCardService2/useRegisterGenuineCard.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MemberCardService2/useUpdateGenuineCardActive.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MemberCardService2/useValidateGenuineCard.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MembershipService2/useCountMemberships.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MembershipService2/useCreate.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MembershipService2/useGet.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MembershipService2/useGetAll.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MembershipService2/useListMemberships.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MembershipService2/usePutPrivileges.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MembershipService2/useUpdate.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MembershipService2/useUpdateActive.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MembershipService2/useUpdatePrivate.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MembershipService2/useUpdateRecurring.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MembershipService2/useUpdateTrial.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MetricService2/useGetCreatedDates.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MetricService2/useGetExceptionPayments.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MetricService2/useGetMembers.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MetricService2/useGetNewKeyHolders.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MetricService2/useGetNewMembers.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MetricService2/useGetPendingAccounts.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MetricService2/useGetRevenue.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MetricService2/useGetTotalKeyHolders.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/MetricService2/useGetTotalMembers.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useCountClients.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useCountUserClients.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useDeleteClient.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useEnableClient.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useGetAccessToken.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useGetClient.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useGetClientDetails.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useGetClientInfo.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useGetRefreshToken.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useListClients.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useListUserClients.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useRegisterClient.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useRevokeRefreshToken.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useSaveAccessToken.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useSaveRefreshToken.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useUpdateClient.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/OAuthService2/useUpdateClientExpiry.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PaymentService2/useCountPayments.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PaymentService2/useCountUserPayments.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PaymentService2/useGetPayment.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PaymentService2/useListPayments.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PaymentService2/useListUserPayments.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PaymentService2/useReplayPaymentProcessing.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PinService2/useAccessInstructions.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PinService2/useGeneratePin.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PinService2/useGenerateTemporaryPin.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PinService2/useGetUserPin.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PinService2/useUpdatePin.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PinService2/useUpdateUserPin.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PreferenceService2/useCountSystemPreferences.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PreferenceService2/useDeleteSystemPreference.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PreferenceService2/useGetAllSystemPreferences.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PreferenceService2/useGetSystemPreference.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PreferenceService2/useListSystemPreferences.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PreferenceService2/usePutSystemPreference.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PreferenceService2/usePutSystemPreferencePrivileges.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PreferenceService2/useSystemPreference.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PreferenceService2/useUpdateSystemPreference.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PreferenceService2/useUpdateSystemPreferenceEnabled.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PrivilegeService2/useCountPrivileges.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PrivilegeService2/useCreatePrivilege.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PrivilegeService2/useDeletePrivilege.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PrivilegeService2/useGetAllPrivileges.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PrivilegeService2/useGetAllSystemPermissions.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PrivilegeService2/useGetPrivilege.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PrivilegeService2/useGetUserPrivileges.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PrivilegeService2/useListPrivileges.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PrivilegeService2/useUpdatePrivilegeDescription.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PrivilegeService2/useUpdatePrivilegeEnabled.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PrivilegeService2/useUpdatePrivilegeIcon.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/PrivilegeService2/useUpdatePrivilegeName.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/StripeEventService2/useCountRecords.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/StripeEventService2/useGet.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/StripeEventService2/useGetAll.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/StripeEventService2/useListRecords.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/SystemPreferenceService2/useCountSystemPreferences.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/SystemPreferenceService2/useDeleteSystemPreference.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/SystemPreferenceService2/useGetAllSystemPreferences.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/SystemPreferenceService2/useGetSystemPreference.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/SystemPreferenceService2/useListSystemPreferences.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/SystemPreferenceService2/usePutSystemPreference.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/SystemPreferenceService2/usePutSystemPreferencePrivileges.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/SystemPreferenceService2/useSystemPreference.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/SystemPreferenceService2/useUpdateSystemPreference.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/SystemPreferenceService2/useUpdateSystemPreferenceEnabled.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useCountUsers.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useCreate.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useGetStanding.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useGetStatuses.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useGetUser.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useGetUserGrantablePrivileges.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useGetUsers.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useGrantPrivilege.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useListUsers.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/usePutUserPrivileges.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useRegister.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useRequestPasswordReset.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useRequestSlackInvite.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useResetPassword.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useRevokePrivilege.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useUpdateCash.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useUpdateEmail.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useUpdateExpiry.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useUpdateMembership.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useUpdateName.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useUpdateNewsletter.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useUpdatePassword.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useUpdatePaymentEmail.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useUpdateStatus.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useUpdateStripeEmail.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/UserService2/useUpdateUsername.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/WebHookService2/useCountHooks.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/WebHookService2/useCountUserHooks.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/WebHookService2/useCreateHook.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/WebHookService2/useDeleteHook.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/WebHookService2/useEnableHook.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/WebHookService2/useGetAllHooks.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/WebHookService2/useGetHook.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/WebHookService2/useGetHooks.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/WebHookService2/useListHooks.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/WebHookService2/useListUserHooks.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/WebHookService2/usePutHookPrivileges.tsx create mode 100644 packages/frontend-react/src/lib/hooks/providers/WebHookService2/useUpdateHook.tsx create mode 100644 packages/frontend-react/src/lib/hooks/useAuth.tsx create mode 100644 packages/frontend-react/src/lib/hooks/useClickOutside.tsx create mode 100644 packages/frontend-react/src/lib/hooks/useInterval.tsx create mode 100644 packages/frontend-react/src/lib/hooks/usePrivilegeCodesReducer.tsx create mode 100644 packages/frontend-react/src/lib/hooks/useSelectorReducer.tsx create mode 100644 packages/frontend-react/src/lib/hooks/useToggleReducer.tsx create mode 100644 packages/frontend-react/src/lib/mocking/data.ts create mode 100644 packages/frontend-react/src/lib/mocking/functions.ts create mode 100644 packages/frontend-react/src/lib/mocking/handlers.ts create mode 100644 packages/frontend-react/src/lib/nomos.ts create mode 100644 packages/frontend-react/src/lib/providers/ApiKeyService2.ts create mode 100644 packages/frontend-react/src/lib/providers/AuthService2.ts create mode 100644 packages/frontend-react/src/lib/providers/CurrentUser2.ts create mode 100644 packages/frontend-react/src/lib/providers/EmailService2.ts create mode 100644 packages/frontend-react/src/lib/providers/EventService2.ts create mode 100644 packages/frontend-react/src/lib/providers/IpnService2.ts create mode 100644 packages/frontend-react/src/lib/providers/KeyService2.ts create mode 100644 packages/frontend-react/src/lib/providers/MemberCardService2.ts create mode 100644 packages/frontend-react/src/lib/providers/MembershipService2.ts create mode 100644 packages/frontend-react/src/lib/providers/MetricService2.ts create mode 100644 packages/frontend-react/src/lib/providers/OAuthService2.ts create mode 100644 packages/frontend-react/src/lib/providers/PaymentService2.ts create mode 100644 packages/frontend-react/src/lib/providers/PinService2.ts create mode 100644 packages/frontend-react/src/lib/providers/PreferenceService2.ts create mode 100644 packages/frontend-react/src/lib/providers/PrivilegeService2.ts create mode 100644 packages/frontend-react/src/lib/providers/StripeEventService2.ts create mode 100644 packages/frontend-react/src/lib/providers/SystemPreferenceService2.ts create mode 100644 packages/frontend-react/src/lib/providers/UserService2.ts create mode 100644 packages/frontend-react/src/lib/providers/WebHookService2.ts create mode 100644 packages/frontend-react/src/lib/ui/common.tsx create mode 100644 packages/frontend-react/src/lib/ui/fontawesome/common.ts create mode 100644 packages/frontend-react/src/lib/ui/fontawesome/index.ts create mode 100644 packages/frontend-react/src/lib/ui/index.ts create mode 100644 packages/frontend-react/src/lib/ui/storybook/common.tsx create mode 100644 packages/frontend-react/src/lib/ui/storybook/index.ts create mode 100644 packages/frontend-react/src/lib/utils/common.ts create mode 100644 packages/frontend-react/src/lib/utils/constants.ts create mode 100644 packages/frontend-react/src/lib/utils/debugging.ts create mode 100644 packages/frontend-react/src/lib/utils/index.ts create mode 100644 packages/frontend-react/src/lib/validators/common.ts create mode 100644 packages/frontend-react/src/lib/validators/custom.ts create mode 100644 packages/frontend-react/src/lib/validators/records.ts create mode 100644 packages/frontend-react/src/main.css create mode 100644 packages/frontend-react/src/main.tsx create mode 100644 packages/frontend-react/src/routeTree.gen.ts create mode 100644 packages/frontend-react/src/router.tsx create mode 100644 packages/frontend-react/src/routes/__root.tsx create mode 100644 packages/frontend-react/src/routes/_admin.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.accesslogs.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.apikeys.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.dashboard.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.databasebackup.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.email.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.emailtemplates.$templateId.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.emailtemplates.new.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.emailtemplates.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.events.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.index.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.ipnrecords.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.logs.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.membercards.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.memberships.$membershipId.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.memberships.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.newsletter.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.oauth.$.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.oauth.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.paymentgateways.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.payments.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.privileges.$privilegeId.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.privileges.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.reports.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.siteconfiguration.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.striperecords.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.systemkeys.$.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.systemkeys.index.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.systempreferences.$preferenceId.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.systempreferences.new.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.systempreferences.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.transactions.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.users.$userId.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.users.new.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.users.tsx create mode 100644 packages/frontend-react/src/routes/_admin/admin.webhooks.tsx create mode 100644 packages/frontend-react/src/routes/_admin/backup.tsx create mode 100644 packages/frontend-react/src/routes/_admin/config.tsx create mode 100644 packages/frontend-react/src/routes/_admin/events.tsx create mode 100644 packages/frontend-react/src/routes/_admin/logs.tsx create mode 100644 packages/frontend-react/src/routes/_admin/oauth.tsx create mode 100644 packages/frontend-react/src/routes/_public.tsx create mode 100644 packages/frontend-react/src/routes/_public/login.tsx create mode 100644 packages/frontend-react/src/routes/_public/recovery.reset.$token.tsx create mode 100644 packages/frontend-react/src/routes/_public/recovery.tsx create mode 100644 packages/frontend-react/src/routes/_public/register.tsx create mode 100644 packages/frontend-react/src/routes/_user.tsx create mode 100644 packages/frontend-react/src/routes/_user/accesslogs.tsx create mode 100644 packages/frontend-react/src/routes/_user/apikeys.$.tsx create mode 100644 packages/frontend-react/src/routes/_user/apikeys.index.tsx create mode 100644 packages/frontend-react/src/routes/_user/dashboard.tsx create mode 100644 packages/frontend-react/src/routes/_user/dooraccess.tsx create mode 100644 packages/frontend-react/src/routes/_user/getinvolved.tsx create mode 100644 packages/frontend-react/src/routes/_user/grants.tsx create mode 100644 packages/frontend-react/src/routes/_user/home.tsx create mode 100644 packages/frontend-react/src/routes/_user/index.tsx create mode 100644 packages/frontend-react/src/routes/_user/logout.tsx create mode 100644 packages/frontend-react/src/routes/_user/membership.tsx create mode 100644 packages/frontend-react/src/routes/_user/profile.tsx create mode 100644 packages/frontend-react/src/routes/_user/purchase.tsx create mode 100644 packages/frontend-react/src/routes/_user/settings.tsx create mode 100644 packages/frontend-react/src/routes/_user/transactions.tsx create mode 100644 packages/frontend-react/src/routes/_user/vhsopen.tsx create mode 100644 packages/frontend-react/src/routes/_user/webhooks.tsx create mode 100644 packages/frontend-react/src/styles/base/common.css create mode 100644 packages/frontend-react/src/styles/base/tables.css create mode 100644 packages/frontend-react/src/styles/components/btn.css create mode 100644 packages/frontend-react/src/styles/components/card.css create mode 100644 packages/frontend-react/src/styles/components/charts.css create mode 100644 packages/frontend-react/src/styles/components/list-group.css create mode 100644 packages/frontend-react/src/styles/components/overlay-container.css create mode 100644 packages/frontend-react/src/styles/components/popover.css create mode 100644 packages/frontend-react/src/styles/components/tabs.css create mode 100644 packages/frontend-react/src/styles/tailwind.css create mode 100644 packages/frontend-react/src/styles/utilities/common.css create mode 100644 packages/frontend-react/src/styles/utilities/find-me.css create mode 100644 packages/frontend-react/src/styles/utilities/layout.css create mode 100644 packages/frontend-react/src/styles/utilities/tables.css create mode 100644 packages/frontend-react/src/types/api.ts create mode 100644 packages/frontend-react/src/types/charts.ts create mode 100644 packages/frontend-react/src/types/common.ts create mode 100644 packages/frontend-react/src/types/db.ts create mode 100644 packages/frontend-react/src/types/fontawesome/common.ts create mode 100644 packages/frontend-react/src/types/fontawesome/index.ts create mode 100644 packages/frontend-react/src/types/providers/IApiKeyService2.ts create mode 100644 packages/frontend-react/src/types/providers/IAuthService2.ts create mode 100644 packages/frontend-react/src/types/providers/IEmailService2.ts create mode 100644 packages/frontend-react/src/types/providers/IEventService2.ts create mode 100644 packages/frontend-react/src/types/providers/IIpnService2.ts create mode 100644 packages/frontend-react/src/types/providers/IKeyService2.ts create mode 100644 packages/frontend-react/src/types/providers/IMemberCardService2.ts create mode 100644 packages/frontend-react/src/types/providers/IMembershipService2.ts create mode 100644 packages/frontend-react/src/types/providers/IMetricService2.ts create mode 100644 packages/frontend-react/src/types/providers/IOAuthService2.ts create mode 100644 packages/frontend-react/src/types/providers/IPaymentService2.ts create mode 100644 packages/frontend-react/src/types/providers/IPinService2.ts create mode 100644 packages/frontend-react/src/types/providers/IPreferenceService2.ts create mode 100644 packages/frontend-react/src/types/providers/IPrivilegeService2.ts create mode 100644 packages/frontend-react/src/types/providers/IStripeEventService2.ts create mode 100644 packages/frontend-react/src/types/providers/ISystemPreferenceService2.ts create mode 100644 packages/frontend-react/src/types/providers/IUserService2.ts create mode 100644 packages/frontend-react/src/types/providers/IWebHookService2.ts create mode 100644 packages/frontend-react/src/types/routing.ts create mode 100644 packages/frontend-react/src/types/ui.ts create mode 100644 packages/frontend-react/src/types/utils.ts create mode 100644 packages/frontend-react/src/types/validators/common.ts create mode 100644 packages/frontend-react/src/types/validators/records.ts create mode 100644 packages/frontend-react/src/vite-env.d.ts create mode 100644 packages/frontend-react/stylelint.config.mjs create mode 100644 packages/frontend-react/tailwind.config.js create mode 100755 packages/frontend-react/tools/bootstrap-config.sh create mode 100755 packages/frontend-react/tools/clean-build-assets.sh create mode 100755 packages/frontend-react/tools/fix-storybook-titles.sh create mode 100755 packages/frontend-react/tools/generate-fa-names.sh create mode 100755 packages/frontend-react/tools/generate-fa-types.sh create mode 100755 packages/frontend-react/tools/generate-provider-implementations.sh create mode 100755 packages/frontend-react/tools/generate-validator-implementations.sh create mode 100644 packages/frontend-react/tsconfig.eslint.json create mode 100644 packages/frontend-react/tsconfig.json create mode 100644 packages/frontend-react/tsconfig.node.json create mode 100644 packages/frontend-react/tsr.config.json create mode 100644 packages/frontend-react/vite.config.ts rename .bowerrc => packages/frontend-web/.bowerrc (100%) create mode 100644 packages/frontend-web/.prettierignore rename bower.json => packages/frontend-web/bower.json (100%) create mode 100644 packages/frontend-web/conf/nginx-vhost-docker-compose.conf create mode 100644 packages/frontend-web/conf/nginx-vhost-docker.conf create mode 100644 packages/frontend-web/conf/nginx-vhost-windows.conf create mode 100644 packages/frontend-web/justfile create mode 100644 packages/frontend-web/package.json rename {tools => packages/frontend-web/tools}/bower.sh (67%) rename {web => packages/frontend-web/web}/admin/accesslogs/accesslogs.html (100%) rename {web => packages/frontend-web/web}/admin/accesslogs/accesslogs.js (100%) rename {web => packages/frontend-web/web}/admin/admin.html (100%) rename {web => packages/frontend-web/web}/admin/admin.js (100%) rename {web => packages/frontend-web/web}/admin/apikeys/apikeys.html (100%) rename {web => packages/frontend-web/web}/admin/apikeys/apikeys.js (100%) rename {web => packages/frontend-web/web}/admin/apikeys/details.html (100%) rename {web => packages/frontend-web/web}/admin/apikeys/details.js (100%) rename {web => packages/frontend-web/web}/admin/backup/backup.html (100%) rename {web => packages/frontend-web/web}/admin/backup/backup.js (100%) rename {web => packages/frontend-web/web}/admin/config/config.html (100%) rename {web => packages/frontend-web/web}/admin/config/config.js (100%) rename {web => packages/frontend-web/web}/admin/email/email.html (100%) rename {web => packages/frontend-web/web}/admin/email/email.js (100%) rename {web => packages/frontend-web/web}/admin/events/events.html (100%) rename {web => packages/frontend-web/web}/admin/events/events.js (100%) rename {web => packages/frontend-web/web}/admin/home/home.html (100%) rename {web => packages/frontend-web/web}/admin/home/home.js (100%) rename {web => packages/frontend-web/web}/admin/ipn/records.html (100%) rename {web => packages/frontend-web/web}/admin/ipn/records.js (100%) rename {web => packages/frontend-web/web}/admin/logs/logs.html (100%) rename {web => packages/frontend-web/web}/admin/logs/logs.js (100%) rename {web => packages/frontend-web/web}/admin/membercards/membercards.html (100%) rename {web => packages/frontend-web/web}/admin/membercards/membercards.js (100%) rename {web => packages/frontend-web/web}/admin/memberships/edit.html (100%) rename {web => packages/frontend-web/web}/admin/memberships/edit.js (100%) rename {web => packages/frontend-web/web}/admin/memberships/memberships.html (100%) rename {web => packages/frontend-web/web}/admin/memberships/memberships.js (100%) rename {web => packages/frontend-web/web}/admin/oauth/clients.html (100%) rename {web => packages/frontend-web/web}/admin/oauth/clients.js (100%) rename {web => packages/frontend-web/web}/admin/oauth/oauth.html (100%) rename {web => packages/frontend-web/web}/admin/oauth/oauth.js (100%) rename {web => packages/frontend-web/web}/admin/payments/payments.html (100%) rename {web => packages/frontend-web/web}/admin/payments/payments.js (100%) rename {web => packages/frontend-web/web}/admin/privileges/privileges.html (100%) rename {web => packages/frontend-web/web}/admin/privileges/privileges.js (100%) rename {web => packages/frontend-web/web}/admin/stripe/records.html (100%) rename {web => packages/frontend-web/web}/admin/stripe/records.js (100%) rename {web => packages/frontend-web/web}/admin/systempreferences/systempreferences.html (100%) rename {web => packages/frontend-web/web}/admin/systempreferences/systempreferences.js (100%) rename {web => packages/frontend-web/web}/admin/transactions/transactions.html (100%) rename {web => packages/frontend-web/web}/admin/transactions/transactions.js (100%) rename {web => packages/frontend-web/web}/admin/users/profile.html (100%) rename {web => packages/frontend-web/web}/admin/users/profile.js (100%) rename {web => packages/frontend-web/web}/admin/users/users.html (100%) rename {web => packages/frontend-web/web}/admin/users/users.js (100%) rename {web => packages/frontend-web/web}/admin/webhooks/webhooks.html (100%) rename {web => packages/frontend-web/web}/admin/webhooks/webhooks.js (100%) rename {web => packages/frontend-web/web}/app.css (100%) rename {web => packages/frontend-web/web}/app.js (100%) rename {web => packages/frontend-web/web}/apple-touch-icon-precomposed.png (100%) rename {web => packages/frontend-web/web}/apple-touch-icon.png (100%) create mode 100644 packages/frontend-web/web/badges/cert_cnc_mill_lathe.svg create mode 100644 packages/frontend-web/web/badges/cert_laser.svg create mode 100644 packages/frontend-web/web/badges/door_access.svg create mode 100644 packages/frontend-web/web/badges/key_holder.svg create mode 100644 packages/frontend-web/web/badges/key_holder_inactive.svg create mode 100644 packages/frontend-web/web/badges/laser.png create mode 100644 packages/frontend-web/web/badges/newsletter.svg create mode 100644 packages/frontend-web/web/favicon.ico rename {web => packages/frontend-web/web}/gateways/moneybookers/MoneyBookers.png (100%) rename {web => packages/frontend-web/web}/gateways/moneybookers/MoneyBookers_big.png (100%) rename {web => packages/frontend-web/web}/gateways/paypal/PayPal.png (100%) rename {web => packages/frontend-web/web}/gateways/paypal/PayPal_big.png (100%) create mode 100644 packages/frontend-web/web/images/logo.png create mode 100644 packages/frontend-web/web/images/provider/github.png create mode 100644 packages/frontend-web/web/images/provider/google.png create mode 100644 packages/frontend-web/web/images/provider/pin.png create mode 100644 packages/frontend-web/web/images/provider/rfid.png create mode 100644 packages/frontend-web/web/images/provider/slack.png create mode 100644 packages/frontend-web/web/images/slack.png create mode 100644 packages/frontend-web/web/images/vhs-member-card-2015-2-full.png create mode 100644 packages/frontend-web/web/images/vhs-member-card-2015-2-thumb.png create mode 100644 packages/frontend-web/web/images/vhs-member-card-2015-full.png create mode 100644 packages/frontend-web/web/images/vhs-member-card-2015-thumb.png rename {web => packages/frontend-web/web}/index.html (100%) rename {web => packages/frontend-web/web}/providers/apikey.js (100%) rename {web => packages/frontend-web/web}/providers/auth.js (100%) rename {web => packages/frontend-web/web}/providers/currentUser.js (100%) rename {web => packages/frontend-web/web}/providers/email.js (100%) rename {web => packages/frontend-web/web}/providers/event.js (100%) rename {web => packages/frontend-web/web}/providers/ipn.js (100%) rename {web => packages/frontend-web/web}/providers/membercard.js (100%) rename {web => packages/frontend-web/web}/providers/membership.js (100%) rename {web => packages/frontend-web/web}/providers/metric.js (100%) rename {web => packages/frontend-web/web}/providers/payment.js (100%) rename {web => packages/frontend-web/web}/providers/pin.js (100%) rename {web => packages/frontend-web/web}/providers/preference.js (100%) rename {web => packages/frontend-web/web}/providers/privilege.js (100%) rename {web => packages/frontend-web/web}/providers/stripe.js (100%) rename {web => packages/frontend-web/web}/providers/user.js (100%) rename {web => packages/frontend-web/web}/providers/webhook.js (100%) rename {web => packages/frontend-web/web}/public/login/login.html (100%) rename {web => packages/frontend-web/web}/public/login/login.js (100%) rename {web => packages/frontend-web/web}/public/public.js (100%) rename {web => packages/frontend-web/web}/public/recovery/recovery.html (100%) rename {web => packages/frontend-web/web}/public/recovery/recovery.js (100%) rename {web => packages/frontend-web/web}/public/recovery/reset.html (100%) rename {web => packages/frontend-web/web}/public/recovery/reset.js (100%) rename {web => packages/frontend-web/web}/public/register/register.html (100%) rename {web => packages/frontend-web/web}/public/register/register.js (100%) rename {web => packages/frontend-web/web}/setup/img (100%) rename {web => packages/frontend-web/web}/setup/license.html (100%) rename {web => packages/frontend-web/web}/setup/style.css (100%) rename {web => packages/frontend-web/web}/user/accesslogs/accesslogs.html (100%) rename {web => packages/frontend-web/web}/user/accesslogs/accesslogs.js (100%) rename {web => packages/frontend-web/web}/user/apikeys/apikeys.html (100%) rename {web => packages/frontend-web/web}/user/apikeys/apikeys.js (100%) rename {web => packages/frontend-web/web}/user/apikeys/details.html (100%) rename {web => packages/frontend-web/web}/user/apikeys/details.js (100%) rename {web => packages/frontend-web/web}/user/dashboard/dashboard.html (100%) rename {web => packages/frontend-web/web}/user/dashboard/dashboard.js (100%) rename {web => packages/frontend-web/web}/user/dooraccess/dooraccess.html (100%) rename {web => packages/frontend-web/web}/user/dooraccess/dooraccess.js (100%) rename {web => packages/frontend-web/web}/user/getinvolved/getinvolved.html (100%) rename {web => packages/frontend-web/web}/user/getinvolved/getinvolved.js (100%) rename {web => packages/frontend-web/web}/user/grants/grants.html (100%) rename {web => packages/frontend-web/web}/user/grants/grants.js (100%) create mode 100644 packages/frontend-web/web/user/home/home.html rename {web => packages/frontend-web/web}/user/home/home.js (100%) rename {web => packages/frontend-web/web}/user/membership/membership.html (100%) rename {web => packages/frontend-web/web}/user/membership/membership.js (100%) rename {web => packages/frontend-web/web}/user/profile/profile.html (100%) rename {web => packages/frontend-web/web}/user/profile/profile.js (100%) rename {web => packages/frontend-web/web}/user/purchase/purchase.html (100%) rename {web => packages/frontend-web/web}/user/purchase/purchase.js (100%) rename {web => packages/frontend-web/web}/user/settings/settings.html (100%) rename {web => packages/frontend-web/web}/user/settings/settings.js (100%) rename {web => packages/frontend-web/web}/user/transactions/transactions.html (100%) rename {web => packages/frontend-web/web}/user/transactions/transactions.js (100%) rename {web => packages/frontend-web/web}/user/user.html (100%) rename {web => packages/frontend-web/web}/user/user.js (100%) rename {web => packages/frontend-web/web}/user/vhsopen/vhsopen.html (100%) rename {web => packages/frontend-web/web}/user/vhsopen/vhsopen.js (100%) rename {web => packages/frontend-web/web}/user/webhooks/webhooks.html (100%) rename {web => packages/frontend-web/web}/user/webhooks/webhooks.js (100%) create mode 100644 pnpm-lock.yaml delete mode 100644 tests/ConstantsTest.php delete mode 100644 tests/schema/RingSchema.php delete mode 100644 tests/schema/SwordEnchantmentsSchema.php delete mode 100755 tools/ghetto_deploy.sh delete mode 100755 tools/make-webhook-key.sh delete mode 100755 tools/vagrant_provision.sh delete mode 100644 tsconfig.json delete mode 100644 vhs/Logger.php delete mode 100644 vhs/database/IDataInterface.php delete mode 100644 vhs/database/ITableGenerator.php delete mode 100644 vhs/database/access/IAccess.php delete mode 100644 vhs/database/joins/IJoinGenerator.php delete mode 100644 vhs/database/joins/Join.php delete mode 100644 vhs/database/offsets/Offset.php delete mode 100644 vhs/database/queries/IQueryGenerator.php delete mode 100644 vhs/database/queries/Query.php delete mode 100644 vhs/database/types/ITypeConverter.php delete mode 100644 vhs/database/types/ITypeGenerator.php delete mode 100644 vhs/database/wheres/IWhereGenerator.php delete mode 100644 vhs/domain/Filter.php delete mode 100644 vhs/domain/collections/DomainCollection.php delete mode 100644 vhs/messaging/ConnectionInfo.php delete mode 100644 vhs/messaging/Engine.php delete mode 100644 vhs/messaging/IMessagingInterface.php delete mode 100644 vhs/messaging/MessageQueue.php delete mode 100644 vhs/messaging/engines/RabbitMQ/RabbitMQConnectionInfo.php delete mode 100644 vhs/messaging/engines/RabbitMQ/RabbitMQEngine.php delete mode 100644 vhs/messaging/exceptions/MessageQueueException.php delete mode 100644 vhs/monitors/Monitor.php delete mode 100644 vhs/security/IPrincipal.php delete mode 100644 vhs/security/exceptions/InvalidCredentials.php delete mode 100644 vhs/security/exceptions/UnauthorizedException.php delete mode 100644 vhs/services/Service.php delete mode 100644 vhs/services/endpoints/IEndpoint.php delete mode 100644 vhs/services/exceptions/InvalidRequestException.php delete mode 100644 vhs/web/IHttpModule.php delete mode 100644 vhs/web/modules/HttpJsonServiceHandlerModule.php delete mode 120000 web/assets delete mode 120000 web/cache delete mode 120000 web/theme delete mode 120000 web/uploads delete mode 100644 webhooker/.gitignore delete mode 100644 webhooker/README.md delete mode 100644 webhooker/config.docker.js delete mode 100644 webhooker/config.js.template.js delete mode 100644 webhooker/domains.js delete mode 100644 webhooker/events.js delete mode 100644 webhooker/nomos.js delete mode 100644 webhooker/package-lock.json delete mode 100644 webhooker/package.json delete mode 100644 webhooker/test/domains.js delete mode 100644 webhooker/test/webhooks.js delete mode 100755 webhooker/webhooker.console delete mode 100644 webhooker/webhooker.js delete mode 100755 webhooker/webhooker.sbin delete mode 100644 webhooker/webhooker.service delete mode 100644 webhooker/webhooks.js diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 397849576..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,67 +0,0 @@ -# This configuration was automatically generated from a CircleCI 1.0 config. -# It should include any build commands you had along with commands that CircleCI -# inferred from your project structure. We strongly recommend you read all the -# comments in this file to understand the structure of CircleCI 2.0, as the idiom -# for configuration has changed substantially in 2.0 to allow arbitrary jobs rather -# than the prescribed lifecycle of 1.0. In general, we recommend using this generated -# configuration as a reference rather than using it in production, though in most -# cases it should duplicate the execution of your original 1.0 config. -version: 2 -jobs: - build: - working_directory: ~/vhs/nomos - parallelism: 1 - shell: /bin/bash --login - # CircleCI 2.0 does not support environment variables that refer to each other the same way as 1.0 did. - # If any of these refer to each other, rewrite them so that they don't or see https://circleci.com/docs/2.0/env-vars/#interpolating-environment-variables-to-set-other-environment-variables . - environment: - CIRCLE_ARTIFACTS: /tmp/circleci-artifacts - CIRCLE_TEST_REPORTS: /tmp/circleci-test-results - # In CircleCI 1.0 we used a pre-configured image with a large number of languages and other packages. - # In CircleCI 2.0 you can now specify your own image, or use one of our pre-configured images. - # The following configuration line tells CircleCI to use the specified docker image as the runtime environment for you job. - # We have selected a pre-built image that mirrors the build environment we use on - # the 1.0 platform, but we recommend you choose an image more tailored to the needs - # of each job. For more information on choosing an image (or alternatively using a - # VM instead of a container) see https://circleci.com/docs/2.0/executor-types/ - # To see the list of pre-built images that CircleCI provides for most common languages see - # https://circleci.com/docs/2.0/circleci-images/ - docker: - - image: cimg/php:8.3-node - steps: - - checkout - - - run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS - - - restore_cache: - keys: - - v1-dep-{{ .Branch }}- - - v1-dep-master- - - v1-dep- - - - run: npm ci - - - save_cache: - key: v1-dep-{{ .Branch }}-{{ epoch }} - paths: - # This is a broad list of cache paths to include many possible development environments - # You can probably delete some of these entries - - vendor/ - - ~/virtualenvs - - ~/.m2 - - ~/.ivy2 - - ~/.bundle - - ~/.go_workspace - - ~/.gradle - - ~/.cache/bower - - - run: npm run test - - - store_test_results: - path: /tmp/circleci-test-results - - - store_artifacts: - path: /tmp/circleci-artifacts - - - store_artifacts: - path: /tmp/circleci-test-results diff --git a/.dockerignore b/.dockerignore index 6caca9278..b5fc5777f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,10 +1,17 @@ -.phpunit.cache/ +.env* +.git/ +.php*.cache +.phpunit.cache +.wireit/ +*.log data/ logs/ node_modules/ -tools/composer.phar -vendor/ -web/components/ -.php*.cache -.env* -*.log +packages/backend-php/docs/ +packages/backend-php/tools/composer.phar +packages/backend-php/vendor/ +packages/frontend-web/web/components/ +packages/typescript +build-cache/ +packages/*/node_modules/ +packages/*/.wireit/ diff --git a/.editorconfig b/.editorconfig index fe8d9bff8..7f0cd96c7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,13 +1,13 @@ # EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file -root = true +root=true [*] -indent_style = space -indent_size = 4 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true -print_width = 120 +indent_style=space +indent_size=4 +end_of_line=lf +charset=utf-8 +trim_trailing_whitespace=true +insert_final_newline=true +print_width=120 diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 882d87b72..be0c91907 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -19,10 +19,8 @@ jobs: build: strategy: matrix: - component: [backend, frontend, webhooker] - + component: [backend-php, frontend-react, frontend-web] runs-on: ubuntu-latest - env: DOCKERHUB_TEMPLATE: ${{ vars.DOCKERHUB_TEMPLATE_PREFIX }}-${{ matrix.component }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 000000000..e3ffd6a88 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,50 @@ +name: Run Tests + +on: + push: + branches: + - 'main' + - 'master' + - 'trunk' + tags: + - 'v*' + pull_request: + branches: + - 'main' + - 'master' + - 'trunk' + workflow_dispatch: + +jobs: + test-php: + strategy: + matrix: + package: [backend-php] + node-version: ['22'] + runs-on: ubuntu-latest + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + - name: Checkout + uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Use Node.js ${{matrix.node-version}} + uses: actions/setup-node@v4 + with: + node-version: ${{matrix.node-version}} + cache: 'pnpm' + - name: Install dependencies + run: pnpm install + - name: Test ${{matrix.package}} + run: pnpm --filter "./packages/${{matrix.package}}/" test + + shellcheck: + name: Shellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master diff --git a/.gitignore b/.gitignore index 50ae5cee0..727368db3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,251 +1,26 @@ conf/config.ini.php -tools/rabbitmq-signing-key-public.asc* -backup -*.gz -*.zip -logs/*.log -logs/*.log.* - -################# -## Eclipse -################# - -*.pydevproject -.project -.metadata -bin/ -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.classpath -.settings/ -.loadpath -.idea/ - -# External tool builders -.externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" -*.launch - -# CDT-specific -.cproject - -# PDT-specific -.buildpath - -################# -## Visual Studio -################# - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.sln.docstates - -# Build results - -[Dd]ebug/ -x64/ -build/ -[Bb]in/ -[Oo]bj/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -*_i.c -*_p.c -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.log -*.scc - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -*.ncrunch* -.*crunch*.local.xml - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.Publish.xml -*.pubxml -*.publishproj - -# NuGet Packages Directory -## TODO: If you have NuGet Package Restore enabled, uncomment the next line -#packages/ - -# Windows Azure Build Output -csx -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Others -sql/ -*.Cache -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.[Pp]ublish.xml -*.pfx -*.publishsettings - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file to a newer -# Visual Studio version. Backup files are not needed, because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -App_Data/*.mdf -App_Data/*.ldf - -############# -## Windows detritus -############# - -# Windows image file caches -Thumbs.db -ehthumbs.db - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Mac crap -.DS_Store - -############# -## Python -############# - -*.py[cod] - -# Packages -*.egg -*.egg-info -build/ -eggs/ -parts/ -var/ -develop-eggs/ -.installed.cfg - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -.tox - -#Translations -*.mo - -#Mr Developer -.mr.developer.cfg - -#Vagrant -.vagrant - -#Composer -composer.phar -vendor - -#Docker env -docker/nomos.env +docker-compose/nomos.env +tools/composer.phar +nomos.d.ts +logs/ +data/ -#Coverage -coverage.xml -coverage/. +.php-cs-fixer.cache +.phpunit.result.cache -data/** -node_modules docker-compose.conf +node_modules/ +vendor/ -.vscode -conf/config.js - -.php-cs-fixer.cache -.phpunit.cache -.phpunit.result.cache +packages/frontend-react/clean-dist-assets.txt +packages/frontend-react/dist/ +packages/frontend-react/public/assets/ +packages/frontend-react/public/config.json +packages/frontend-react/src/lib/ui/fontawesome/generated.ts +packages/frontend-react/src/types/fontawesome/generated.ts +packages/frontend-react/.wireit/ -tools/composer.phar -.wireit -docker-compose.override.yml +packages/frontend-web/web/components/ -web/components/ +packages/backend-php/conf/mysql-password-mode.cnf +packages/backend-php/tools/composer.phar diff --git a/.husky/pre-commit b/.husky/pre-commit index 8b1424571..4b4b14c0b 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,23 +1,5 @@ #!/usr/bin/env sh -FILES=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') +cd "$(dirname "$(realpath "$0")")/../" || exit 255 -if [ "${FILES}" != "" ]; then - PHP_FILES=$(echo "${FILES}" | grep '\.php' | xargs) - WEBHOOKER_FILES=$(echo "${FILES}" | grep 'webhooker/' | xargs) - - if [ "${PHP_FILES}" != "" ]; then - FILES=$(echo "${PHP_FILES}" | xargs) npm exec just format php - npm exec just test php - fi - - if [ "${WEBHOOKER_FILES}" != "" ]; then - npm exec just test webhooker - fi - - FILES=$(echo "${FILES}" | xargs) npm exec just format all - - git update-index --again - - exit 0 -fi +pnpm exec just git_hook_pre_commit diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 000000000..069ba6213 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +cd "$(dirname "$(realpath "$0")")/../" || exit 255 + +pnpm exec just git_hook_pre_push diff --git a/.prettierignore b/.prettierignore index c0f209081..7ffcca452 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,26 +1,10 @@ .phpunit.cache/ -data/ -node_modules/ -vendor/ -web/components/ -package-lock.json composer.lock -web/badges/cert_cnc_mill_lathe.svg -web/badges/cert_laser.svg -web/badges/door_access.svg -web/badges/key_holder.svg -web/badges/key_holder_inactive.svg -web/badges/newsletter.svg +data/ +diff-report.txt +icons.psd justfile migrations/1/ -migrations/2/ -migrations/3/ -migrations/4/ -migrations/5/ -migrations/6/ -migrations/7/ -migrations/8/ -migrations/9/ migrations/10/ migrations/11/ migrations/12/ @@ -31,6 +15,7 @@ migrations/16/ migrations/17/ migrations/18/ migrations/19/ +migrations/2/ migrations/20/ migrations/21/ migrations/22/ @@ -38,6 +23,35 @@ migrations/23/ migrations/24/ migrations/25/ migrations/26/ -web/components/bower/datatables/media/images/Sorting icons.psd -web/components/bower/datatables/media/images/Sorting -icons.psd +migrations/3/ +migrations/4/ +migrations/5/ +migrations/6/ +migrations/7/ +migrations/8/ +migrations/9/ +node_modules/ +package-lock.json +packages/frontend-react/*.svg +packages/frontend-react/pnpm-lock.yaml +packages/frontend-react/public/assets/ +packages/frontend-react/src/routeTree.gen.ts +packages/frontend-react/src/types/nomos.d.ts +packages/frontend-web/web/badges/cert_cnc_mill_lathe.svg +packages/frontend-web/web/badges/cert_laser.svg +packages/frontend-web/web/badges/door_access.svg +packages/frontend-web/web/badges/key_holder_inactive.svg +packages/frontend-web/web/badges/key_holder.svg +packages/frontend-web/web/badges/newsletter.svg +packages/frontend-web/web/components/ +packages/frontend-web/web/components/bower/datatables/media/images/Sorting +packages/frontend-web/web/components/bower/datatables/media/images/Sorting icons.psd +packages/frontend-web/web/gateways/moneybookers/MoneyBookers_big.png +packages/frontend-web/web/gateways/moneybookers/MoneyBookers.png +packages/frontend-web/web/gateways/paypal/PayPal_big.png +packages/frontend-web/web/gateways/paypal/PayPal.png +packages/frontend-web/web/setup/img +packages/frontend-web/web/setup/license.html +packages/frontend-web/web/setup/style.css +pnpm-lock.yaml +vendor/ diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index d0c4c626f..000000000 --- a/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -FROM ubuntu:xenial - -RUN apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y \ - curl \ - git \ - nginx \ - php7.0 \ - php7.0-bcmath \ - php7.0-cli \ - php7.0-curl \ - php7.0-dom \ - php7.0-fpm \ - php7.0-mbstring \ - php7.0-mysqlnd \ - && apt-get clean && rm -r /var/lib/apt/lists/* - -COPY ["composer.json", "/www/"] - -RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/www/ \ - && cd /www/ \ - && php composer.phar install \ - && echo "daemon off;" >> /etc/nginx/nginx.conf - -COPY ["app", "/www/app"] -COPY ["web", "/www/web"] -COPY ["vhs", "/www/vhs"] -COPY ["migrations", "/www/migrations"] -COPY ["tools", "/www/tools"] -COPY ["conf/nginx-vhost-docker.conf", "/etc/nginx/sites-enabled/default"] -COPY ["docker", "/usr/bin"] -COPY ["conf/config.docker.ini.php", "/www/conf/config.ini.php"] - -CMD ["/usr/bin/docker_run.sh"] diff --git a/README.md b/README.md index 6b2a2ec96..ac4aeef09 100644 --- a/README.md +++ b/README.md @@ -4,62 +4,62 @@ _In greek mythology, Nomos is the personified spirit of law._ This system in a way acts as the rule set for how things are governed, via membership levels and privileges. -## Development +## Status / branches -See here for complete setup, API, and philosophy: -https://github.com/vhs/nomos/wiki +- Nomos is deployed to https://membership.vanhack.ca, but not this version. +- The current deploy of nomos is `tags/last-known-good-early-2025` +- some other tags of note: + - `tags/simplify-docker` is before dynamic docker-compose stuff was removed. + - `tags/before-webhook-removal` is before the webhooker and rabbitmq services were removed -For the old development guide, see: -https://github.com/vhs/nomos/wiki/Contributing - -### Requirements +## Requirements For development, you'll need the following components/dependencies: -- Docker/Docker Compose -- NodeJS/NPM -- PHP 8.2, and extensions (php-xml, php-curl, php-bcmath, php-zip, php-mbstring) +- Docker and Docker Compose +- NodeJS and PNPM +- PHP 8.3, and some extensions (`php-xml`, `php-curl`, `php-dom`, `php-bcmath`, `php-zip`, `php-mbstring`) - jq (`apt install jq`) -All other development dependencies (such as bower, composer, eslint, husky, php-cs-fixer, phpunit, prettier, etc.) will automatically be installed upon running `npm install` after checkout. - -### Development setup guide - -Install the requirements: [docker and docker-compose](https://docs.docker.com/engine/install/), nodejs, php 8.2 (`apt-get install php8.2`), php extensions (`apt-get install php-xml php-curl php-bcmath php-zip`) - -On Mac/Windows, you probably want [Docker Desktop](https://docs.docker.com/get-docker/), which is a fancy app that makes and manages a Linux virtual machine that it runs Docker in. +All other development dependencies (just, bower, composer, husky, php-cs-fixer, phpunit, prettier, etc.) will automatically be installed upon running `pnpm install` after checkout. + +## Development setup guide + +1. Install the requirements: + - [docker and docker-compose](https://docs.docker.com/engine/install/) + - [nodejs](https://nodejs.org/en/download) (v20 or newer) + - [pnpm](https://pnpm.io) - a package manager like npm, that handles repos with multiple packages, like this one + - `sudo npm install --global corepack@latest` + - `corepack enable pnpm` + - php 8.2 (`apt-get install php8.3`) + - php extensions (`apt-get install php-xml php-curl php-bcmath php-zip php-mbstring`) +2. Create a docker.env file + - copy `docker-compose/nomos.env.template` to `docker-compose/nomos.env`. +3. Grant write permissions to the logs directory: + - `chmod a+w logs` + - This is needed is because the back-end PHP code runs as a non-root user inside the container. +4. install dependencies + - run `pnpm install` in the root directory +5. run docker + - `pnpm start` will run all packages + - `pnpm start:db` will run only the database server + - `pnpm start:backend` will run only the php backend + - `pnpm start:frontend-web` will run the legacy Angular frontend + - `pnpm start:frontend-react` will run the newer React frontend + - `pnpm build` will re-build the docker images and then run all packages -- Create a docker-compose configuration file: - - Copy `docker-compose.dev.conf` to `docker-compose.conf` - - You can also use `docker-compose.template.conf` or `docker-compose.sample.conf` as a starting place. - - Edit your new `docker-compose.conf` to customize what services are enabled -- Create a docker.env file - - copy `docker/nomos.env.template` to `docker/nomos.env`. -- run `npm install` in the root directory -- Run `./docker-compose.sh` as a 1:1 wrapper for docker-compose, or generate a - local `docker-compose.yml` file for direct usage with `docker-compose` with - `./docker-compose.sh config > docker-compose.yml` - -Grant write permission to all users on the log directory: `chmod a+w logs`. The -reason this is needed is because the back-end PHP code runs as a non-root user -inside the container, and by default permissions don't grant write access to -non-owners of directories. +The username is `vhs` and the password is `password`. -Start the service with `./docker-compose.sh up`. This should bring everything up, -but the webhook service will still be failing, which is expected. +## dev cycle -To get the webhook service working, run `tools/make-webhook-key.sh` in another terminal, which will provide the correct value of `NOMOS_RABBITMQ_NOMOS_TOKEN`. Then, edit that into -`docker/nomos.env`. +With the docker containers running, you should be able to view nomos at http://127.0.0.1/ -Once you have done this, press Ctrl-C in the terminal with `./docker-compose.sh up`, -then run `./docker-compose.sh up` again. +By default, frontend-react is mounted at `/` and frontend-web is mounted at `/v1/` -You're all set! You can get the address to access the Nomos service from your docker host system by running the following in a separate terminal as `docker-compose.sh`: +If you make changes to `packages/backend-php` or `packages/frontend-web` you'll need to re-start the docker container for them to take effect. -``` -$ docker inspect nomos-frontend | jq -r '.[0].NetworkSettings.Networks | to_entries | .[0].value.IPAddress' -``` +If you make changes to `packages/frontend-react` you'll need to `cd packages/frontend-react && pnpm run build` for changes to take effect. -Or make your docker-compose.conf include `docker-compose/core.ports.yml`, to proxy port 80 of your docker network to port 80 on your host machine. +## old info: -The username is `vhs` and the password is `password`. +Info in the wiki is out of date, but if you're going deep it might be worth taking a look: https://github.com/vhs/nomos/wiki diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index 3e8b6376e..000000000 --- a/Vagrantfile +++ /dev/null @@ -1,76 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# All Vagrant configuration is done below. The "2" in Vagrant.configure -# configures the configuration version (we support older styles for -# backwards compatibility). Please don't change it unless you know what -# you're doing. -Vagrant.configure(2) do |config| - # The most common configuration options are documented and commented below. - # For a complete reference, please see the online documentation at - # https://docs.vagrantup.com. - - print "Using 64 bit image\n" - config.vm.box = "ubuntu/bionic64" - - # Disable automatic box update checking. If you disable this, then - # boxes will only be checked for updates when the user runs - # `vagrant box outdated`. This is not recommended. - # config.vm.box_check_update = false - - # Create a forwarded port mapping which allows access to a specific port - # within the machine from a port on the host machine. In the example below, - # accessing "localhost:8080" will access port 80 on the guest machine. - # config.vm.network "forwarded_port", guest: 80, host: 8080 - - # Create a private network, which allows host-only access to the machine - # using a specific IP. - config.vm.network "private_network", ip: "192.168.38.10" - - # Create a public network, which generally matched to bridged network. - # Bridged networks make the machine appear as another physical device on - # your network. - # config.vm.network "public_network" - - # Share an additional folder to the guest VM. The first argument is - # the path on the host to the actual folder. The second argument is - # the path on the guest to mount the folder. And the optional third - # argument is a set of non-required options. - # config.vm.synced_folder "../data", "/vagrant_data" - - # Provider-specific configuration so you can fine-tune various - # backing providers for Vagrant. These expose provider-specific options. - # Example for VirtualBox: - # - config.vm.provider "virtualbox" do |vb| - # # Display the VirtualBox GUI when booting the machine - # vb.gui = true - # - # # Customize the amount of memory on the VM: - vb.memory = "1024" - end - config.vm.provider "parallels" do |v| - v.name = "nomos" - # v.update_guest_tools = true - # v.check_guest_tools = false - # v.optimize_power_consumption = false - v.memory = 1024 - # v.cpus = 2 - end - # - # View the documentation for the provider you are using for more - # information on available options. - - # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies - # such as FTP and Heroku are also available. See the documentation at - # https://docs.vagrantup.com/v2/push/atlas.html for more information. - # config.push.define "atlas" do |push| - # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" - # end - - # Enable provisioning with a shell script. Additional provisioners such as - # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the - # documentation for more information about their specific syntax and use. - config.vm.provision "shell", path: "tools/vagrant_provision.sh" - -end diff --git a/app/constants/DateTime.php b/app/constants/DateTime.php deleted file mode 100644 index 3700b788b..000000000 --- a/app/constants/DateTime.php +++ /dev/null @@ -1,8 +0,0 @@ -code, $code)); - - if (!empty($val)) { - return $val[0]; - } - - return null; - } - - public static function generate($code, $context) { - $template = self::findByCode($code); - - if (is_null($template)) { - return null; - } - - $engine = new \StringTemplate\Engine('{{', '}}'); - - $ret = []; - - $ret['subject'] = $engine->render($template->subject, $context); - $ret['txt'] = $engine->render($template->body, $context); - $ret['html'] = $engine->render($template->html, $context); - - return $ret; - } - - public function validate(ValidationResults &$results) { - // not required at this time - } -} diff --git a/app/domain/Event.php b/app/domain/Event.php deleted file mode 100644 index c901befa2..000000000 --- a/app/domain/Event.php +++ /dev/null @@ -1,41 +0,0 @@ -Columns()->domain, $domain), Where::Equal(Event::Schema()->Columns()->event, $event)) - ); - - return count($events) > 0; - } - - /** - * @param ValidationResults $results - * - * @return bool - */ - public function validate(ValidationResults &$results) { - // TODO: Implement validate() method. - } -} diff --git a/app/domain/GenuineCard.php b/app/domain/GenuineCard.php deleted file mode 100644 index 663dfd7aa..000000000 --- a/app/domain/GenuineCard.php +++ /dev/null @@ -1,33 +0,0 @@ -key, $key)); - } - - public function validate(ValidationResults &$results) { - } -} diff --git a/app/domain/Ipn.php b/app/domain/Ipn.php deleted file mode 100644 index 1353e3de0..000000000 --- a/app/domain/Ipn.php +++ /dev/null @@ -1,23 +0,0 @@ -type, 'api'), Where::Equal(KeySchema::Columns()->key, $key))); - } - - public static function findByPin($pin) { - return self::where(Where::_And(Where::Equal(KeySchema::Columns()->type, 'pin'), Where::Equal(KeySchema::Columns()->key, $pin))); - } - - public static function findByRfid($rfid) { - return self::where(Where::_And(Where::Equal(KeySchema::Columns()->type, 'rfid'), Where::Equal(KeySchema::Columns()->key, $rfid))); - } - - public static function findByService($service, $key) { - return self::where(Where::_And(Where::Equal(KeySchema::Columns()->type, $service), Where::Equal(KeySchema::Columns()->key, $key))); - } - - public static function findByTypes(...$types) { - return self::where(Where::In(KeySchema::Columns()->type, $types)); - } - - public static function findKeyAndType($key, $type) { - return self::where(Where::_And(Where::Equal(KeySchema::Columns()->type, $type), Where::Equal(KeySchema::Columns()->key, $key))); - } - - public static function getSystemApiKeys() { - return self::where(Where::_And(Where::Null(KeySchema::Columns()->userid), Where::Equal(KeySchema::Columns()->type, 'api'))); - } - - public static function getUserApiKeys($userid) { - return self::where(Where::_And(Where::Equal(Key::Schema()->Columns()->type, 'api'), Where::Equal(Key::Schema()->Columns()->userid, $userid))); - } - - public function getAbsolutePrivileges() { - $privs = []; - - foreach ($this->privileges->all() as $priv) { - if ($priv->code === 'inherit' && $this->userid != null) { - $user = User::find($this->userid); - - if ($user != null) { - foreach ($user->privileges->all() as $userpriv) { - array_push($privs, $userpriv); - } - - if (!is_null($user->membership)) { - foreach ($user->membership->privileges->all() as $mempriv) { - array_push($privs, $mempriv); - } - } - } - } - - array_push($privs, $priv); - } - - $retval = []; - - foreach (array_unique($privs) as $priv) { - //hack array_unique may convert to object - array_push($retval, $priv); - } - - return $retval; - } - - public function validate(ValidationResults &$results) { - } -} diff --git a/app/domain/PasswordResetRequest.php b/app/domain/PasswordResetRequest.php deleted file mode 100644 index fb5c28070..000000000 --- a/app/domain/PasswordResetRequest.php +++ /dev/null @@ -1,28 +0,0 @@ -token, $token)); - } - - public function validate(ValidationResults &$results) { - } -} diff --git a/app/domain/Payment.php b/app/domain/Payment.php deleted file mode 100644 index cbcd58139..000000000 --- a/app/domain/Payment.php +++ /dev/null @@ -1,32 +0,0 @@ -txn_id, $txn_id)) - ); - } - - public function validate(ValidationResults &$results) { - } -} diff --git a/app/domain/StripeEvent.php b/app/domain/StripeEvent.php deleted file mode 100644 index 2da03197f..000000000 --- a/app/domain/StripeEvent.php +++ /dev/null @@ -1,21 +0,0 @@ -Columns()->domain, $domain), Where::Equal(WebHook::Schema()->Columns()->event, $event)) - ); - } - - /** - * @param ValidationResults $results - * - * @return bool - */ - public function validate(ValidationResults &$results) { - // TODO: Implement validate() method. - } -} diff --git a/app/exceptions/InvalidInputException.php b/app/exceptions/InvalidInputException.php deleted file mode 100644 index 8c95d6064..000000000 --- a/app/exceptions/InvalidInputException.php +++ /dev/null @@ -1,9 +0,0 @@ -domain, $event->event, json_encode($data)); - } - - public function Init(Logger &$logger = null) { - $events = Event::findAll(); - - foreach ($events as $event) { - if (!$event->enabled) { - continue; - } - - /** @var Domain $domainClass */ - $domainClass = '\\app\\domain\\' . $event->domain; - - /** - * Updated 2025/01/30 with all valid domain triggers and are returned via the GetEventTypes service method in the EventService1. - */ - switch ($event->event) { - case 'before:changed': - /** - * Intentionally disabled to prevent excessive event generation due to low level triggering. - * I.e. Enabling this is noisy AF because it triggers on everything minor field update. - **/ - // $domainClass::onAnyBeforeChange(function (...$args) use ($event) { - // $this->fireEvent($event, $args); - // }); - break; - - case 'before:created': - MessageQueue::ensure($event->domain, $event->event); - $domainClass::onAnyBeforeCreate(function (...$args) use ($event) { - $this->fireEvent($event, $args); - }); - break; - - case 'before:deleted': - MessageQueue::ensure($event->domain, $event->event); - $domainClass::onAnyBeforeDelete(function (...$args) use ($event) { - $this->fireEvent($event, $args); - }); - break; - - case 'before:updated': - MessageQueue::ensure($event->domain, $event->event); - $domainClass::onAnyBeforeUpdate(function (...$args) use ($event) { - $this->fireEvent($event, $args); - }); - break; - - case 'before:saved': - MessageQueue::ensure($event->domain, $event->event); - $domainClass::onAnyBeforeSave(function (...$args) use ($event) { - $this->fireEvent($event, $args); - }); - break; - - case 'changed': - /** - * Intentionally disabled to prevent excessive event generation due to low level triggering. - * I.e. Enabling this is noisy AF because it triggers on everything minor field update. - **/ - // $domainClass::onAnyChanged(function (...$args) use ($event) { - // $this->fireEvent($event, $args); - // }); - break; - - case 'created': - MessageQueue::ensure($event->domain, $event->event); - $domainClass::onAnyCreated(function (...$args) use ($event) { - $this->fireEvent($event, $args); - }); - break; - - case 'deleted': - MessageQueue::ensure($event->domain, $event->event); - $domainClass::onAnyDeleted(function (...$args) use ($event) { - $this->fireEvent($event, $args); - }); - break; - - case 'updated': - MessageQueue::ensure($event->domain, $event->event); - $domainClass::onAnyUpdated(function (...$args) use ($event) { - $this->fireEvent($event, $args); - }); - break; - - case 'saved': - MessageQueue::ensure($event->domain, $event->event); - $domainClass::onAnySaved(function (...$args) use ($event) { - $this->fireEvent($event, $args); - }); - break; - - default: - $logger->log(sprintf('Invalid domain event [%s] for domain [%s]', $event->event, $event->domain)); - break; - } - } - } -} diff --git a/app/schema/AccessTokenSchema.php b/app/schema/AccessTokenSchema.php deleted file mode 100644 index 2a9d0b892..000000000 --- a/app/schema/AccessTokenSchema.php +++ /dev/null @@ -1,38 +0,0 @@ -addColumn('id', Type::Int(false, 0)); - $table->addColumn('token', Type::String()); - $table->addColumn('expires', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('userid', Type::Int()); - $table->addColumn('appclientid', Type::Int()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id), - Constraint::ForeignKey($table->columns->appclientid, AppClientSchema::Table(), AppClientSchema::Columns()->id) - ); - - $table->setAccess(PrivilegedAccess::GenerateAccess('accesstoken', $table)); - - return $table; - } -} diff --git a/app/schema/EventPrivilegeSchema.php b/app/schema/EventPrivilegeSchema.php deleted file mode 100644 index 364215645..000000000 --- a/app/schema/EventPrivilegeSchema.php +++ /dev/null @@ -1,35 +0,0 @@ -addColumn('eventid', Type::Int()); - $table->addColumn('privilegeid', Type::Int()); - $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('notes', Type::Text()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->eventid), - Constraint::PrimaryKey($table->columns->privilegeid), - Constraint::ForeignKey($table->columns->eventid, EventSchema::Table(), EventSchema::Columns()->id), - Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) - ); - - return $table; - } -} diff --git a/app/schema/KeyPrivilegeSchema.php b/app/schema/KeyPrivilegeSchema.php deleted file mode 100644 index 7484034ec..000000000 --- a/app/schema/KeyPrivilegeSchema.php +++ /dev/null @@ -1,35 +0,0 @@ -addColumn('keyid', Type::Int()); - $table->addColumn('privilegeid', Type::Int()); - $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('notes', Type::Text()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->keyid), - Constraint::PrimaryKey($table->columns->privilegeid), - Constraint::ForeignKey($table->columns->keyid, KeySchema::Table(), KeySchema::Columns()->id), - Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) - ); - - return $table; - } -} diff --git a/app/schema/MembershipPrivilegeSchema.php b/app/schema/MembershipPrivilegeSchema.php deleted file mode 100644 index ac8795c35..000000000 --- a/app/schema/MembershipPrivilegeSchema.php +++ /dev/null @@ -1,35 +0,0 @@ -addColumn('membershipid', Type::Int()); - $table->addColumn('privilegeid', Type::Int()); - $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('notes', Type::Text()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->membershipid), - Constraint::PrimaryKey($table->columns->privilegeid), - Constraint::ForeignKey($table->columns->membershipid, MembershipSchema::Table(), MembershipSchema::Columns()->id), - Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) - ); - - return $table; - } -} diff --git a/app/schema/RefreshTokenSchema.php b/app/schema/RefreshTokenSchema.php deleted file mode 100644 index 71d799949..000000000 --- a/app/schema/RefreshTokenSchema.php +++ /dev/null @@ -1,38 +0,0 @@ -addColumn('id', Type::Int(false, 0)); - $table->addColumn('token', Type::String()); - $table->addColumn('expires', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('userid', Type::Int()); - $table->addColumn('appclientid', Type::Int()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id), - Constraint::ForeignKey($table->columns->appclientid, AppClientSchema::Table(), AppClientSchema::Columns()->id) - ); - - $table->setAccess(PrivilegedAccess::GenerateAccess('accesstoken', $table, $table->columns->userid)); - - return $table; - } -} diff --git a/app/schema/SystemPreferencePrivilegeSchema.php b/app/schema/SystemPreferencePrivilegeSchema.php deleted file mode 100644 index 616b88320..000000000 --- a/app/schema/SystemPreferencePrivilegeSchema.php +++ /dev/null @@ -1,35 +0,0 @@ -addColumn('systempreferenceid', Type::Int()); - $table->addColumn('privilegeid', Type::Int()); - $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('notes', Type::Text()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->systempreferenceid), - Constraint::PrimaryKey($table->columns->privilegeid), - Constraint::ForeignKey($table->columns->systempreferenceid, SystemPreferenceSchema::Table(), SystemPreferenceSchema::Columns()->id), - Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) - ); - - return $table; - } -} diff --git a/app/schema/UserPrivilegeSchema.php b/app/schema/UserPrivilegeSchema.php deleted file mode 100644 index 446cdd08a..000000000 --- a/app/schema/UserPrivilegeSchema.php +++ /dev/null @@ -1,35 +0,0 @@ -addColumn('userid', Type::Int()); - $table->addColumn('privilegeid', Type::Int()); - $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('notes', Type::Text()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->userid), - Constraint::PrimaryKey($table->columns->privilegeid), - Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id), - Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) - ); - - return $table; - } -} diff --git a/app/schema/WebHookPrivilegeSchema.php b/app/schema/WebHookPrivilegeSchema.php deleted file mode 100644 index d83c59034..000000000 --- a/app/schema/WebHookPrivilegeSchema.php +++ /dev/null @@ -1,35 +0,0 @@ -addColumn('webhookid', Type::Int()); - $table->addColumn('privilegeid', Type::Int()); - $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('notes', Type::Text()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->webhookid), - Constraint::PrimaryKey($table->columns->privilegeid), - Constraint::ForeignKey($table->columns->webhookid, WebHookSchema::Table(), WebHookSchema::Columns()->id), - Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) - ); - - return $table; - } -} diff --git a/app/security/oauth/providers/slack/SlackProviderException.php b/app/security/oauth/providers/slack/SlackProviderException.php deleted file mode 100644 index 4c121621c..000000000 --- a/app/security/oauth/providers/slack/SlackProviderException.php +++ /dev/null @@ -1,12 +0,0 @@ -getStatusCode(), (string) $response->getBody()); - } -} diff --git a/circle.yml b/circle.yml deleted file mode 100644 index b52198641..000000000 --- a/circle.yml +++ /dev/null @@ -1,9 +0,0 @@ -machine: - php: - version: 8.3 -# apparently 7.0.0RC7 causes seg faults when we try to generate coverage.. disabling until circleCI supports php7+ -#test: -# pre: -# - sed -i 's/^;//' ~/.phpenv/versions/$(phpenv global)/etc/conf.d/xdebug.ini -# override: -# - ./vendor/phpunit/phpunit/phpunit --coverage-html $CIRCLE_ARTIFACTS diff --git a/composer.json b/composer.json deleted file mode 100644 index 6c3ec08eb..000000000 --- a/composer.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "require": { - "aws/aws-sdk-php": "3.336.8", - "nicmart/string-template": "0.1.3", - "league/oauth2-client": "2.8.0", - "php-amqplib/php-amqplib": "2.5.2", - "stripe/stripe-php": "7.128.0", - "league/oauth2-github": "3.1.1", - "league/oauth2-google": "4.0.1" - }, - "require-dev": { - "phpunit/phpunit": "11.5.3", - "friendsofphp/php-cs-fixer": "v3.68.0" - } -} diff --git a/conf/config.ini.php.template b/conf/config.ini.php.template deleted file mode 100644 index 1380b538d..000000000 --- a/conf/config.ini.php.template +++ /dev/null @@ -1,55 +0,0 @@ - array( - "item_name" => "VHS Keyholder Membership", - "item_number" => "vhs_membership_keyholder" - ) - )); - - /** - * Show MySql Errors. - * Not recomended for live site. true/false - */ - define('DEBUG', true); diff --git a/conf/mimes.types b/conf/mimes.types deleted file mode 100644 index 7cd68a57c..000000000 --- a/conf/mimes.types +++ /dev/null @@ -1,89 +0,0 @@ - -types { - text/html html htm shtml; - text/css css; - text/xml xml; - image/gif gif; - image/jpeg jpeg jpg; - application/javascript js; - application/atom+xml atom; - application/rss+xml rss; - - text/mathml mml; - text/plain txt; - text/vnd.sun.j2me.app-descriptor jad; - text/vnd.wap.wml wml; - text/x-component htc; - - image/png png; - image/tiff tif tiff; - image/vnd.wap.wbmp wbmp; - image/x-icon ico; - image/x-jng jng; - image/x-ms-bmp bmp; - image/svg+xml svg svgz; - image/webp webp; - - application/font-woff woff; - application/java-archive jar war ear; - application/json json; - application/mac-binhex40 hqx; - application/msword doc; - application/pdf pdf; - application/postscript ps eps ai; - application/rtf rtf; - application/vnd.apple.mpegurl m3u8; - application/vnd.ms-excel xls; - application/vnd.ms-fontobject eot; - application/vnd.ms-powerpoint ppt; - application/vnd.wap.wmlc wmlc; - application/vnd.google-earth.kml+xml kml; - application/vnd.google-earth.kmz kmz; - application/x-7z-compressed 7z; - application/x-cocoa cco; - application/x-java-archive-diff jardiff; - application/x-java-jnlp-file jnlp; - application/x-makeself run; - application/x-perl pl pm; - application/x-pilot prc pdb; - application/x-rar-compressed rar; - application/x-redhat-package-manager rpm; - application/x-sea sea; - application/x-shockwave-flash swf; - application/x-stuffit sit; - application/x-tcl tcl tk; - application/x-x509-ca-cert der pem crt; - application/x-xpinstall xpi; - application/xhtml+xml xhtml; - application/xspf+xml xspf; - application/zip zip; - - application/octet-stream bin exe dll; - application/octet-stream deb; - application/octet-stream dmg; - application/octet-stream iso img; - application/octet-stream msi msp msm; - - application/vnd.openxmlformats-officedocument.wordprocessingml.document docx; - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx; - application/vnd.openxmlformats-officedocument.presentationml.presentation pptx; - - audio/midi mid midi kar; - audio/mpeg mp3; - audio/ogg ogg; - audio/x-m4a m4a; - audio/x-realaudio ra; - - video/3gpp 3gpp 3gp; - video/mp2t ts; - video/mp4 mp4; - video/mpeg mpeg mpg; - video/quicktime mov; - video/webm webm; - video/x-flv flv; - video/x-m4v m4v; - video/x-mng mng; - video/x-ms-asf asx asf; - video/x-ms-wmv wmv; - video/x-msvideo avi; -} diff --git a/conf/nginx-vhost-docker-compose.conf b/conf/nginx-vhost-docker-compose.conf deleted file mode 100644 index 23453705a..000000000 --- a/conf/nginx-vhost-docker-compose.conf +++ /dev/null @@ -1,30 +0,0 @@ -server { - listen 80 default_server; - server_name _; - - location ~ (/services/|\.php$) { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_read_timeout 300; - fastcgi_pass nomos-backend:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME /var/www/html/app/app.php; # ?service=$fastcgi_script_name; - fastcgi_buffers 16 16k; - fastcgi_buffer_size 32k; - include fastcgi_params; - } - - - location / { - root /var/www/html; - } - - - location = /robots.txt { - return 200 "User-agent: *\nDisallow: /"; - } - - location ~* \.php$ { - include fastcgi_params; - fastcgi_pass nomos-backend:9000; - } -} diff --git a/conf/nginx-vhost-vagrant.conf b/conf/nginx-vhost-vagrant.conf deleted file mode 100644 index 40b7b988a..000000000 --- a/conf/nginx-vhost-vagrant.conf +++ /dev/null @@ -1,34 +0,0 @@ -server { - set $app_root "/vagrant"; - - listen 80 ; - server_name _ - membership.vanhack.ca - 192.168.38.10; - - location ~ (/services/|\.php$) { - include fastcgi_params; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_read_timeout 300; - fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $app_root/app/app.php; # ?service=$fastcgi_script_name; - fastcgi_buffers 16 16k; - fastcgi_buffer_size 32k; - } - - - location / { - root $app_root/web; - } - - - location = /robots.txt { - return 200 "User-agent: *\nDisallow: /"; - } - - location ~* \.php$ { - include fastcgi_params; - fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; - } -} \ No newline at end of file diff --git a/conf/nginx-vhost-windows.conf b/conf/nginx-vhost-windows.conf deleted file mode 100644 index 59b4ca2f3..000000000 --- a/conf/nginx-vhost-windows.conf +++ /dev/null @@ -1,56 +0,0 @@ -server { - - set $app_root "D:/Dropbox/Source/VHS/membership-manager-pro"; - - listen 80; - server_name membership.dev.vanhack.ca; - - #for development only - location ~ (/tools/.+\.php)$ { - fastcgi_intercept_errors on; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_read_timeout 300; - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $app_root$fastcgi_script_name; - include fastcgi_params; - } - - location ~ (/services/|\.php$) { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_read_timeout 300; - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $app_root/app/app.php; # ?service=$fastcgi_script_name; - include fastcgi_params; - } - - location / { - root $app_root/web; - } - - location ~* \.php$ { - include fastcgi_params; - fastcgi_pass 127.0.0.1:9000; - } - - location ~ (/assets/) { - root $app_root/scripts; - } - - location ~ (/theme/) { - root $app_root/scripts; - } - - location ~ (/cache/) { - root $app_root/cache; - } - - location ~ (/uploads/) { - root $app_root/uploads; - } - - location ~ (/badges/) { - root $app_root/badges; - } -} diff --git a/conf/nginx.conf b/conf/nginx.conf index eabd1709a..da3e46cc8 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -1,33 +1,50 @@ server { - set $app_root "/var/www/sites/membership.vanhack.ca"; - - listen 80; - server_name membership.vanhack.ca; - - location ~ (/services/) { - include fastcgi_params; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_read_timeout 300; - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $app_root/app/app.php; # ?service=$fastcgi_script_name; - } + listen 80; + listen [::]:80; + server_name localhost; + #access_log /var/log/nginx/host.access.log main; - location / { - root $app_root/web; + location ^~ /v1/ { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Host $host; + proxy_pass http://frontend-web/; } - - location = /robots.txt { - return 200 "User-agent: *\nDisallow: /"; + location / { + proxy_pass http://frontend-react; } - location ~* \.php$ { - include fastcgi_params; - fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; - } - - include mime.types; + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + # error_page 500 502 503 504 /50x.html; + # location = /50x.html { + # root /usr/share/nginx/html; + # } + + # proxy the PHP scripts to Apache listening on 127.0.0.1:80 + # + #location ~ \.php$ { + # proxy_pass http://127.0.0.1; + #} + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + # + #location ~ \.php$ { + # root html; + # fastcgi_pass 127.0.0.1:9000; + # fastcgi_index index.php; + # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; + # include fastcgi_params; + #} + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + # + #location ~ /\.ht { + # deny all; + #} } - diff --git a/docker-compose.dev.conf b/docker-compose.dev.conf deleted file mode 100644 index 9e34f749f..000000000 --- a/docker-compose.dev.conf +++ /dev/null @@ -1,160 +0,0 @@ -################################################################################################### -## ## -## Self-contained development configuration using docker-compose. ## -## This configuration provides an internal mysql and RabbitMQ server. ## -## ## -################################################################################################### - -## Always use the core -docker-compose/core.yml - -## -## Expose port -## -# docker-compose/core.ports.yml - -## -## Lift environments from nomos.env file -## - -docker-compose/files-local-nomos-env.yml - -## -## Build core -## - -docker-compose/build-backend.yml -docker-compose/build-frontend.yml - -## -## Enable bridge network_mode for core services -## -#docker-compose/core.network-bridge.yml - -## -## Enable proxy network for core services -## -#docker-compose/core.network-proxy.yml - -## -## Enable proxy network for core services, managed by docker-compose -## -docker-compose/core.network-proxy-internal.yml - -## -## MySQL -## - -## -## Local MySQL instance -## - -docker-compose/mysql-local.yml -#docker-compose/mysql-local-network-bridge.yml -docker-compose/mysql-local-network-mysql.yml - -## -## External MySQL instance -## - -# docker-compose/mysql-external-mysqld.yml - -## -## External MySQL network (mysql) -## - -# docker-compose/mysql-external-network-mysql.yml - -## -## Inject the MySQL container through EXTERNAL_MYSQL_HOST -## - -# docker-compose/mysql-external-variable.yml - -## -## Webhooker -## -## Webhooker depends on RabbitMQ, so always enable a RabbitMQ -## - -docker-compose/webhooker.yml - -## -## Webhooker logs -## - -docker-compose/webhooker-logs-local.yml - -## -## Build webhooker -## - -docker-compose/webhooker-build.yml - -## -## Enable bridge network_mode for webhooker -## - -# docker-compose/webhooker-network-bridge.yml - -## -## Enable proxy network for webhooker -## - -# docker-compose/webhooker-network-proxy.yml - -## -## Enable rabbitmq network for webhooker -## - -# docker-compose/webhooker-network-rabbitmq.yml - -## -## Local RabbitMQ instance -## - -docker-compose/rabbitmq-local.yml -docker-compose/rabbitmq-local-management.yml -# docker-compose/rabbitmq-local-network-bridge.yml - -# Network not managed with docker-compose -#docker-compose/rabbitmq-local-network-rabbitmq.yml - -# Network managed with docker-compose -docker-compose/rabbitmq-local-network-internal.yml - -## -## External RabbitMQ instance -## - -# docker-compose/rabbitmq-external.yml - -## -## Vhosts -## -## TODO: Example Traefik config -## - -## -## Enable membership.vanhack.ca for nginx-proxy -## - -# docker-compose/vhost-membership-vanhack-ca.yml - -## -## Enable membership.test.vanhack.ca for nginx-proxy -## - -# docker-compose/vhost-membership-test-vanhack-ca.yml - -## -## Override backend files from local filesystem -## - -docker-compose/files-local-backend.yml - -## -## Override frontend files from local filesystem -## - -docker-compose/files-local-frontend.yml diff --git a/docker-compose.sample.conf b/docker-compose.sample.conf deleted file mode 100644 index b85394000..000000000 --- a/docker-compose.sample.conf +++ /dev/null @@ -1,153 +0,0 @@ -################################################################################################### -## ## -## Example configuration that links to an external MySQL server, but has its own RabbitMQ server ## -## ## -################################################################################################### - -## Always use the core -docker-compose/core.yml - -## -## Expose port -## -# docker-compose/core.ports.yml - -## -## Lift environments from nomos.env file -## - -docker-compose/files-local-nomos-env.yml - -## -## Build core -## - -docker-compose/build-backend.yml -docker-compose/build-frontend.yml - -## -## Enable bridge network_mode for core services -## -# docker-compose/core.network-bridge.yml - -## -## Enable proxy network for core services -## -docker-compose/core.network-proxy.yml - -## -## MySQL -## - -## -## Local MySQL instance -## - -# docker-compose/mysql-local.yml -# docker-compose/mysql-local-network-bridge.yml -# docker-compose/mysql-local-network-mysql.yml - -## -## External MySQL instance -## - -docker-compose/mysql-external-mysqld.yml - -## -## External MySQL network (mysql) -## - -docker-compose/mysql-external-network-mysql.yml - -## -## Inject the MySQL container through EXTERNAL_MYSQL_HOST -## - -# docker-compose/mysql-external-variable.yml - -## -## Webhooker -## -## Webhooker depends on RabbitMQ, so always enable a RabbitMQ -## - -docker-compose/webhooker.yml - -## -## Webhooker logs -## - -docker-compose/webhooker-logs-local.yml - -## -## Build webhooker -## - -docker-compose/webhooker-build.yml - -## -## Enable bridge network_mode for webhooker -## - -# docker-compose/webhooker-network-bridge.yml - -## -## Enable proxy network for webhooker -## - -# docker-compose/webhooker-network-proxy.yml - -## -## Enable rabbitmq network for webhooker -## - -docker-compose/webhooker-network-rabbitmq.yml - -## -## Local RabbitMQ instance -## - -docker-compose/rabbitmq-local.yml -docker-compose/rabbitmq-local-management.yml -# docker-compose/rabbitmq-local-network-bridge.yml -docker-compose/rabbitmq-local-network-rabbitmq.yml - -## -## External RabbitMQ instance -## - -# docker-compose/rabbitmq-external.yml - -## -## Vhosts -## - -## Enable membership.vanhack.ca - -## Traefik -# docker-compose/traefik-prod.yml - -## nginx-proxy -# docker-compose/nginx-proxy-prod.yml - -## -## Enable membership.test.vanhack.ca -## - -## Traefik -# docker-compose/traefik-devtest.yml - -## nginx-proxy -# docker-compose/nginx-proxy-devtest.yml - -## -## Override backend files from local filesystem -## - -docker-compose/files-local-backend.yml - -## -## Override frontend files from local filesystem -## - -docker-compose/files-local-frontend.yml diff --git a/docker-compose.sh b/docker-compose.sh deleted file mode 100755 index 23b598380..000000000 --- a/docker-compose.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -SCRIPT_DIR=$(realpath "$(dirname "$0")") - -COMPOSE_CMD=docker-compose - -if docker compose > /dev/null 2>&1; then - COMPOSE_CMD="docker compose" -fi - -COMPOSE_FILE=$(grep -E -v '^(#|;|$)' docker-compose.conf | xargs | tr ' ' ':') -export COMPOSE_FILE - -# --env-file is passed in here to make the variables inside available for -# expansion *inside rules definitions*. This is separate from the -# service.SVC.env_file directive, which defines the environment *inside* the -# container. - -# shellcheck disable=SC2086 -/usr/bin/env ${COMPOSE_CMD} -p nomos --env-file "${SCRIPT_DIR}/docker/nomos.env" "$@" diff --git a/docker-compose.template.conf b/docker-compose.template.conf deleted file mode 100644 index ea2ca7f98..000000000 --- a/docker-compose.template.conf +++ /dev/null @@ -1,157 +0,0 @@ -## Always use the core -docker-compose/core.yml - -## -## Expose port -## -# docker-compose/core.ports.yml - -## -## Lift environments from nomos.env file -## - -#docker-compose/files-local-nomos-env.yml - -## -## Build core -## - -#docker-compose/build-backend.yml -#docker-compose/build-frontend.yml - -## -## Enable bridge network_mode for core services -## -# docker-compose/core.network-bridge.yml - -## -## Enable proxy network for core services -## -#docker-compose/core.network-proxy.yml - -## -## Enable proxy network for core services, managed by docker-compose -## -#docker-compose/core.network-proxy-internal.yml - -## -## MySQL -## - -## -## Local MySQL instance -## - -# docker-compose/mysql-local.yml -# docker-compose/mysql-local-network-bridge.yml -# docker-compose/mysql-local-network-mysql.yml - -## -## External MySQL instance -## - -#docker-compose/mysql-external-mysqld.yml - -## -## External MySQL network (mysql) -## - -#docker-compose/mysql-external-network-mysql.yml - -## -## Inject the MySQL container through EXTERNAL_MYSQL_HOST -## - -# docker-compose/mysql-external-variable.yml - -## -## Webhooker -## -## Webhooker depends on RabbitMQ, so always enable a RabbitMQ -## - -#docker-compose/webhooker.yml - -## -## Webhooker logs -## - -#docker-compose/webhooker-logs-local.yml - -## -## Build webhooker -## - -#docker-compose/webhooker-build.yml - -## -## Enable bridge network_mode for webhooker -## - -# docker-compose/webhooker-network-bridge.yml - -## -## Enable proxy network for webhooker -## - -# docker-compose/webhooker-network-proxy.yml - -## -## Enable rabbitmq network for webhooker -## - -#docker-compose/webhooker-network-rabbitmq.yml - -## -## Local RabbitMQ instance -## - -#docker-compose/rabbitmq-local.yml -#docker-compose/rabbitmq-local-management.yml -# docker-compose/rabbitmq-local-network-bridge.yml - -# Network not managed with docker-compose -#docker-compose/rabbitmq-local-network-rabbitmq.yml - -# Network managed with docker-compose -# docker-compose/rabbitmq-local-network-internal.yml - -## -## External RabbitMQ instance -## - -# docker-compose/rabbitmq-external.yml - -## -## Vhosts -## - -## Enable membership.vanhack.ca - -## Traefik -# docker-compose/traefik-prod.yml - -## nginx-proxy -# docker-compose/nginx-proxy-prod.yml - -## -## Enable membership.test.vanhack.ca -## - -## Traefik -# docker-compose/traefik-devtest.yml - -## nginx-proxy -# docker-compose/nginx-proxy-devtest.yml - -## -## Override backend files from local filesystem -## - -#docker-compose/files-local-backend.yml - -## -## Override frontend files from local filesystem -## - -#docker-compose/files-local-frontend.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..2b13d244d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,98 @@ +# this docker-compose file deprecates the old docker-compose.sh and all the yml files in the docker-compose directory +name: nomos +services: + backend-php: + #image: vanhack/nomos-backend-php + build: + context: ${PWD} + dockerfile: docker-compose/Dockerfile + target: backend-php + depends_on: + db-mariadb: + condition: service_healthy + networks: + - backend + - database + restart: always + env_file: + - ${PWD}/docker-compose/nomos.env + volumes: + - ${PWD}/data/backup:/var/www/html/backup + - ${PWD}/packages/backend-php/app:/var/www/html/app + - ${PWD}/migrations:/var/www/html/migrations + - ${PWD}/packages/backend-php/vhs:/var/www/html/vhs + - ${PWD}/logs:/var/www/html/logs + + frontend-react: + #image: vanhack/nomos-frontend-react + build: + context: ${PWD} + dockerfile: docker-compose/Dockerfile + target: frontend-react + depends_on: + backend-php: + condition: service_started + links: + - backend-php:backend-php + networks: + - backend + restart: always + volumes: + - ${PWD}/packages/frontend-react/dist:/var/www/html + + frontend-web: + #image: vanhack/nomos-frontend-web + build: + context: ${PWD}/ + dockerfile: docker-compose/Dockerfile + target: frontend-web + depends_on: + backend-php: + condition: service_started + links: + - backend-php:backend-php + networks: + - backend + restart: always + volumes: + - ${PWD}/packages/frontend-web/web:/var/www/html + + db-mariadb: + image: mariadb:11.4 + env_file: + - ${PWD}/docker-compose/nomos.env + environment: + - MARIADB_USER=${NOMOS_DB_USER} + - MARIADB_PASSWORD=${NOMOS_DB_PASSWORD} + - MARIADB_DATABASE=${NOMOS_DB_DATABASE} + - MARIADB_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MARIADB_AUTO_UPGRADE=1 + networks: + - database + volumes: + - ${PWD}/data/mysql:/var/lib/mysql + healthcheck: + test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'] + start_period: 10s + interval: 10s + timeout: 5s + retries: 10 + + proxy: + image: nginx:latest + depends_on: + frontend-web: + condition: service_started + frontend-react: + condition: service_started + networks: + backend: null + ports: + - '80:80' + restart: always + volumes: + - ${PWD}/conf/nginx.conf:/etc/nginx/conf.d/default.conf + +networks: + backend: + database: diff --git a/docker-compose/.dockerignore b/docker-compose/.dockerignore new file mode 100644 index 000000000..aa3a93b8d --- /dev/null +++ b/docker-compose/.dockerignore @@ -0,0 +1,5 @@ +Dockerfile +.dockerignore +node_modules +npm-debug.log +dist diff --git a/docker-compose/Dockerfile b/docker-compose/Dockerfile index 9c7d17ddf..aa6858d7d 100644 --- a/docker-compose/Dockerfile +++ b/docker-compose/Dockerfile @@ -4,63 +4,113 @@ FROM cimg/php:${PHP_VERSION}-node AS build USER 1001:1002 -WORKDIR /build/ +WORKDIR /build -ENV DEBIAN_FRONTEND=noninteractive +COPY --link ./ ./ -COPY --chown=circleci:circleci ./ ./ +FROM node:22-alpine AS base -RUN npm ci && cd webhooker && npm ci +ENV PNPM_HOME="/pnpm" -FROM php:${PHP_VERSION}-fpm AS backend +ENV PATH="$PNPM_HOME:$PATH" -VOLUME ["/sessions"] +RUN npm install -g npm@latest && npm install -g corepack@latest + +FROM base AS build-base + +RUN apk --no-cache add just mariadb-client php83-bcmath php83-common php83-curl php83-dom php83-iconv php83-mbstring php83-mysqli php83-mysqlnd php83-openssl php83-phar php83-session php83-simplexml php83-sockets php83-zip php83 python3 wget alpine-sdk bash git php83-tokenizer php83-xml php83-xmlwriter + +# Commands expect a "php" binary, but alpine packages are versioned. Create a symlink to satisfy this expectation. +RUN ln -s /usr/bin/php83 /usr/bin/php + +FROM build-base AS build + +ENV COMPOSER_INSTALL_OPT=--no-dev + +ENV CI=true + +RUN mkdir /app/ /build/ && chown 1000:1000 /build/ /app/ + +USER 1000:1000 + +COPY --from=source --chown=1000:1000 --link /build /build + +WORKDIR /build + +RUN --mount=type=cache,id=nomos-pnpm,target=/pnpm/store,uid=1000,gid=1000 pnpm install --frozen-lockfile && pnpm -r build + +FROM scratch AS build-backend-php + +WORKDIR /build + +COPY --from=build --link /build/packages/backend-php/ /app/backend-php/ + +FROM scratch AS build-frontend-react -RUN mkdir /sessions && chown www-data:www-data /sessions +WORKDIR /build -ENV DEBIAN_FRONTEND=noninteractive +COPY --from=build --link /build/packages/frontend-react/conf/ /app/frontend-react/conf/ + +COPY --from=build --link /build/packages/frontend-react/dist/ /app/frontend-react/dist/ + +FROM scratch AS build-frontend-web + +WORKDIR /build + +COPY --from=build --link /build/packages/frontend-web/ /app/frontend-web/ + +FROM alpine:3 AS backend-php-base + +RUN apk --no-cache add just mariadb-client php83-bcmath php83-common php83-curl php83-dom php83-iconv php83-mbstring php83-mysqli php83-mysqlnd php83-openssl php83-phar php83-session php83-simplexml php83-sockets php83-zip php83 python3 wget php83-fpm + +RUN ln -s /usr/bin/php83 /usr/bin/php + +RUN sed -i 's/^listen/;listen/g' /etc/php83/php-fpm.d/www.conf + +FROM backend-php-base AS backend-php + +VOLUME ["/sessions"] + +EXPOSE 9000/tcp WORKDIR /var/www/html -ENTRYPOINT [ "docker_compose_run.sh" ] -CMD ["php-fpm"] +VOLUME ["/var/www/html/backup"] + +RUN mkdir -p /var/www/html/backup && chown nobody:nobody /var/www/html/backup + +ENTRYPOINT ["docker_compose_run.sh"] + +CMD ["php-fpm83"] + +RUN mkdir -p /var/www/html/conf/ -RUN apt-get update \ - && apt-get install -y git libzip-dev mariadb-client zip \ - && docker-php-ext-install -j$(nproc) bcmath mysqli zip \ - && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* +COPY --from=source --link /build/migrations/ migrations/ -COPY --from=build /build/app/ app/ -COPY --from=build /build/migrations/ migrations/ -COPY --from=build /build/tools/ tools/ -COPY --from=build /build/vhs/ vhs/ -COPY --from=build /build/vendor/ vendor/ +COPY --from=build-backend-php --link /app/backend-php/app/ app/ -COPY --from=build /build/docker/*.sh /usr/local/bin/ -COPY --from=build /build/conf/config.docker.ini.php /var/www/html/conf/config.ini.php +COPY --from=build-backend-php --link /app/backend-php/conf/config.docker.ini.php conf/config.ini.php -RUN cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \ - && echo "session.save_path = \"/sessions\"" >> "$PHP_INI_DIR/php.ini" +COPY --from=build-backend-php --link /app/backend-php/tools/ tools/ -FROM nginx:stable-alpine AS frontend +COPY --from=build-backend-php --link /app/backend-php/vhs/ vhs/ -COPY conf/nginx-vhost-docker-compose.conf /etc/nginx/conf.d/default.conf +COPY --from=build-backend-php --link /app/backend-php/vendor/ vendor/ -COPY --from=build /build/web/ /var/www/html/ +COPY --from=build-backend-php --link /app/backend-php/conf/php/*.ini /etc/php83/conf.d/ -FROM build AS webhooker-build +COPY --from=build-backend-php --link /app/backend-php/conf/php-fpm/*.conf /etc/php83/php-fpm.d/ -WORKDIR /build/webhooker +COPY --from=build-backend-php --link /app/backend-php/docker/*.sh /usr/local/bin/ -RUN chmod 755 webhooker.sbin webhooker.console \ - && cp config.docker.js config.js +FROM nginx:stable-alpine AS frontend-react -FROM node:lts AS webhooker +COPY --from=build-frontend-react --link /app/frontend-react/conf/nginx-react-docker-compose.conf /etc/nginx/conf.d/default.conf -RUN mkdir -p /app/logs /app/webhooker +COPY --from=build-frontend-react --link /app/frontend-react/dist/ /var/www/html/ -COPY --from=webhooker-build /build/webhooker/ /app/webhooker/ +FROM nginx:stable-alpine AS frontend-web -WORKDIR /app/webhooker +COPY --from=build-frontend-web --link /app/frontend-web/conf/nginx-vhost-docker-compose.conf /etc/nginx/conf.d/default.conf -CMD ["/app/webhooker/webhooker.sbin"] +COPY --from=build-frontend-web --link /app/frontend-web/web/ /var/www/html/ diff --git a/docker-compose/build-backend.yml b/docker-compose/build-backend.yml deleted file mode 100644 index 4db5651b6..000000000 --- a/docker-compose/build-backend.yml +++ /dev/null @@ -1,6 +0,0 @@ -services: - nomos-backend: - build: - context: .. - dockerfile: docker-compose/Dockerfile - target: backend diff --git a/docker-compose/build-frontend.yml b/docker-compose/build-frontend.yml deleted file mode 100644 index c133cca03..000000000 --- a/docker-compose/build-frontend.yml +++ /dev/null @@ -1,6 +0,0 @@ -services: - nomos-frontend: - build: - context: .. - dockerfile: docker-compose/Dockerfile - target: frontend diff --git a/docker-compose/core.network-bridge.yml b/docker-compose/core.network-bridge.yml deleted file mode 100644 index 626903605..000000000 --- a/docker-compose/core.network-bridge.yml +++ /dev/null @@ -1,6 +0,0 @@ -services: - nomos-frontend: - network_mode: bridge - - nomos-backend: - network_mode: bridge diff --git a/docker-compose/core.network-proxy-internal.yml b/docker-compose/core.network-proxy-internal.yml deleted file mode 100644 index 87c608753..000000000 --- a/docker-compose/core.network-proxy-internal.yml +++ /dev/null @@ -1,15 +0,0 @@ -services: - nomos-frontend: - networks: - - proxy - - nomos-backend: - networks: - - proxy - - nomos-webhooker: - networks: - - proxy - -networks: - proxy: diff --git a/docker-compose/core.network-proxy.yml b/docker-compose/core.network-proxy.yml deleted file mode 100644 index b09da5f6e..000000000 --- a/docker-compose/core.network-proxy.yml +++ /dev/null @@ -1,12 +0,0 @@ -services: - nomos-frontend: - networks: - - proxy - - nomos-backend: - networks: - - proxy - -networks: - proxy: - external: true diff --git a/docker-compose/core.ports.yml b/docker-compose/core.ports.yml deleted file mode 100644 index e310c996d..000000000 --- a/docker-compose/core.ports.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - nomos-frontend: - ports: - - 80:80 diff --git a/docker-compose/core.yml b/docker-compose/core.yml deleted file mode 100644 index b514ccd21..000000000 --- a/docker-compose/core.yml +++ /dev/null @@ -1,14 +0,0 @@ -services: - nomos-frontend: - image: vanhack/nomos-frontend - container_name: nomos-frontend - depends_on: - - nomos-backend - restart: always - links: - - nomos-backend:nomos-backend - - nomos-backend: - image: vanhack/nomos-backend - container_name: nomos-backend - restart: always diff --git a/docker-compose/files-local-backend.yml b/docker-compose/files-local-backend.yml deleted file mode 100644 index 792745065..000000000 --- a/docker-compose/files-local-backend.yml +++ /dev/null @@ -1,7 +0,0 @@ -services: - nomos-backend: - volumes: - - ../app:/var/www/html/app - - ../migrations:/var/www/html/migrations - - ../vhs:/var/www/html/vhs - - ../logs:/var/www/html/logs diff --git a/docker-compose/files-local-frontend.yml b/docker-compose/files-local-frontend.yml deleted file mode 100644 index fb73f7e35..000000000 --- a/docker-compose/files-local-frontend.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - nomos-frontend: - volumes: - - ../web:/var/www/html - - ../conf/nginx-vhost-docker-compose.conf:/etc/nginx/conf.d/default.conf diff --git a/docker-compose/files-local-nomos-env.yml b/docker-compose/files-local-nomos-env.yml deleted file mode 100644 index d145159a1..000000000 --- a/docker-compose/files-local-nomos-env.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - nomos-backend: - env_file: - - ../docker/nomos.env diff --git a/docker-compose/mysql-external-mysqld.yml b/docker-compose/mysql-external-mysqld.yml deleted file mode 100644 index c9872b610..000000000 --- a/docker-compose/mysql-external-mysqld.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - nomos-backend: - external_links: - - mysqld:mysqld diff --git a/docker-compose/mysql-external-network-mysql-lithium.yml b/docker-compose/mysql-external-network-mysql-lithium.yml deleted file mode 100644 index 532b98285..000000000 --- a/docker-compose/mysql-external-network-mysql-lithium.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - nomos-backend: - networks: - - mysql - -networks: - mysql: - external: true - name: mysql-lithium diff --git a/docker-compose/mysql-external-network-mysql.yml b/docker-compose/mysql-external-network-mysql.yml deleted file mode 100644 index 232ff5da0..000000000 --- a/docker-compose/mysql-external-network-mysql.yml +++ /dev/null @@ -1,8 +0,0 @@ -services: - nomos-backend: - networks: - - mysql - -networks: - mysql: - external: true diff --git a/docker-compose/mysql-external-variable.yml b/docker-compose/mysql-external-variable.yml deleted file mode 100644 index 9383a0d15..000000000 --- a/docker-compose/mysql-external-variable.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - nomos-backend: - external_links: - - ${EXTERNAL_MYSQL_HOST}:nomos-mysql diff --git a/docker-compose/mysql-local-network-bridge.yml b/docker-compose/mysql-local-network-bridge.yml deleted file mode 100644 index 955ca9f63..000000000 --- a/docker-compose/mysql-local-network-bridge.yml +++ /dev/null @@ -1,3 +0,0 @@ -services: - nomos-mysql: - network_mode: bridge diff --git a/docker-compose/mysql-local-network-mysql.yml b/docker-compose/mysql-local-network-mysql.yml deleted file mode 100644 index a93e5a36b..000000000 --- a/docker-compose/mysql-local-network-mysql.yml +++ /dev/null @@ -1,11 +0,0 @@ -services: - nomos-backend: - networks: - - mysql - - nomos-mysql: - networks: - - mysql - -networks: - mysql: diff --git a/docker-compose/mysql-local.yml b/docker-compose/mysql-local.yml deleted file mode 100644 index 4ba705b34..000000000 --- a/docker-compose/mysql-local.yml +++ /dev/null @@ -1,22 +0,0 @@ -services: - # backend needs the db started first - nomos-backend: - depends_on: - nomos-mysql: - condition: service_started - nomos-mysql: - image: mysql:5.7 - container_name: nomos-mysql - environment: - - MYSQL_USER=${NOMOS_DB_USER} - - MYSQL_PASSWORD=${NOMOS_DB_PASSWORD} - - MYSQL_DATABASE=${NOMOS_DB_DATABASE} - - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - volumes: - - ../data/mysql:/var/lib/mysql - # Workaround this bug in the mysql image: https://github.com/docker-library/mysql/issues/579 - ulimits: - nproc: 65535 - nofile: - soft: 20000 - hard: 40000 diff --git a/docker-compose/nginx-proxy-devtest.yml b/docker-compose/nginx-proxy-devtest.yml deleted file mode 100644 index 1d76d0e75..000000000 --- a/docker-compose/nginx-proxy-devtest.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - nomos-frontend: - environment: - VIRTUAL_HOST: membership.test.vanhack.ca - LETSENCRYPT_HOST: membership.test.vanhack.ca diff --git a/docker-compose/nginx-proxy-prod.yml b/docker-compose/nginx-proxy-prod.yml deleted file mode 100644 index e4fa0cbb5..000000000 --- a/docker-compose/nginx-proxy-prod.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - nomos-frontend: - environment: - VIRTUAL_HOST: membership.vanhack.ca - LETSENCRYPT_HOST: membership.vanhack.ca diff --git a/docker/nomos.env.template b/docker-compose/nomos.env.template similarity index 63% rename from docker/nomos.env.template rename to docker-compose/nomos.env.template index 2a71364c2..53ac23c43 100644 --- a/docker/nomos.env.template +++ b/docker-compose/nomos.env.template @@ -1,4 +1,4 @@ -NOMOS_DB_SERVER=nomos-mysql +NOMOS_DB_SERVER=db-mariadb NOMOS_DB_USER=nomos NOMOS_DB_PASSWORD=Password1 NOMOS_DB_DATABASE=nomos @@ -20,11 +20,3 @@ NOMOS_STRIPE_PRODUCTS= NOMOS_SLACK_INVITE_TOKEN=foobar NOMOS_SLACK_URL=vanhack.slack.com - -NOMOS_RABBITMQ_HOST=nomos-rabbitmq -NOMOS_RABBITMQ_PORT=5672 -NOMOS_RABBITMQ_USER=nomos -NOMOS_RABBITMQ_PASSWORD=Password1 -NOMOS_RABBITMQ_VHOST=nomos -NOMOS_RABBITMQ_NOMOS_HOST= -NOMOS_RABBITMQ_NOMOS_TOKEN=please-run-tools-slash-make-webhook-key-dot-sh diff --git a/docker-compose/override.yml b/docker-compose/override.yml deleted file mode 100644 index 5803ce7ce..000000000 --- a/docker-compose/override.yml +++ /dev/null @@ -1,13 +0,0 @@ -services: - nomos-rabbitmq: - labels: - - 'com.centurylinklabs.watchtower.enable=false' - nomos-webhooker: - labels: - - 'com.centurylinklabs.watchtower.enable=false' - nomos-backend: - labels: - - 'com.centurylinklabs.watchtower.enable=false' - nomos-frontend: - labels: - - 'com.centurylinklabs.watchtower.enable=false' diff --git a/docker-compose/rabbitmq-external.yml b/docker-compose/rabbitmq-external.yml deleted file mode 100644 index 28215ef74..000000000 --- a/docker-compose/rabbitmq-external.yml +++ /dev/null @@ -1,16 +0,0 @@ -services: - nomos-webhooker: - external_links: - - rabbitmq:nomos-rabbitmq - networks: - - rabbitmq - - nomos-backend: - external_links: - - rabbitmq:nomos-rabbitmq - networks: - - rabbitmq - -networks: - rabbitmq: - external: true diff --git a/docker-compose/rabbitmq-local-management.yml b/docker-compose/rabbitmq-local-management.yml deleted file mode 100644 index 64242d047..000000000 --- a/docker-compose/rabbitmq-local-management.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - nomos-rabbitmq: - ports: - - 15672:15672 diff --git a/docker-compose/rabbitmq-local-network-bridge.yml b/docker-compose/rabbitmq-local-network-bridge.yml deleted file mode 100644 index 50a4c7d48..000000000 --- a/docker-compose/rabbitmq-local-network-bridge.yml +++ /dev/null @@ -1,6 +0,0 @@ -services: - nomos-rabbitmq: - network_mode: bridge - - nomos-webhooker: - network_mode: bridge diff --git a/docker-compose/rabbitmq-local-network-internal.yml b/docker-compose/rabbitmq-local-network-internal.yml deleted file mode 100644 index 592248038..000000000 --- a/docker-compose/rabbitmq-local-network-internal.yml +++ /dev/null @@ -1,15 +0,0 @@ -services: - nomos-rabbitmq: - networks: - - rabbitmq - - nomos-webhooker: - networks: - - rabbitmq - - nomos-backend: - networks: - - rabbitmq - -networks: - rabbitmq: diff --git a/docker-compose/rabbitmq-local-network-rabbitmq.yml b/docker-compose/rabbitmq-local-network-rabbitmq.yml deleted file mode 100644 index 66a6012a3..000000000 --- a/docker-compose/rabbitmq-local-network-rabbitmq.yml +++ /dev/null @@ -1,16 +0,0 @@ -services: - nomos-rabbitmq: - networks: - - rabbitmq - - nomos-webhooker: - networks: - - rabbitmq - - nomos-backend: - networks: - - rabbitmq - -networks: - rabbitmq: - external: true diff --git a/docker-compose/rabbitmq-local.yml b/docker-compose/rabbitmq-local.yml deleted file mode 100644 index 2e10d434b..000000000 --- a/docker-compose/rabbitmq-local.yml +++ /dev/null @@ -1,21 +0,0 @@ -services: - nomos-rabbitmq: - image: rabbitmq:3-management - container_name: nomos-rabbitmq - restart: always - env_file: - - ../docker/nomos.env - environment: - - RABBITMQ_DEFAULT_USER=${NOMOS_RABBITMQ_USER} - - RABBITMQ_DEFAULT_PASS=${NOMOS_RABBITMQ_PASSWORD} - - RABBITMQ_DEFAULT_VHOST=${NOMOS_RABBITMQ_VHOST} - volumes: - - ../data/rabbitmq:/var/lib/rabbitmq - - nomos-webhooker: - depends_on: - - nomos-rabbitmq - - nomos-backend: - depends_on: - - nomos-rabbitmq diff --git a/docker-compose/traefik-devtest.yml b/docker-compose/traefik-devtest.yml deleted file mode 100644 index 1deb85ec3..000000000 --- a/docker-compose/traefik-devtest.yml +++ /dev/null @@ -1,10 +0,0 @@ -services: - nomos-frontend: - labels: - - 'traefik.enable=true' - - 'traefik.http.routers.nomos-devtest.rule=Host(`membership.test.vanhack.ca`) || Host(`membership.devtest.vanhack.ca`)' - - 'traefik.http.routers.nomos-devtest.entryPoints=websecure' - - 'traefik.http.routers.nomos-devtest.tls=true' - - 'traefik.http.routers.nomos-devtest.tls.certresolver=lets-encrypt' - - 'traefik.port=80' - - 'traefik.docker.network=proxy' diff --git a/docker-compose/traefik-prod.yml b/docker-compose/traefik-prod.yml deleted file mode 100644 index 24e800173..000000000 --- a/docker-compose/traefik-prod.yml +++ /dev/null @@ -1,10 +0,0 @@ -services: - nomos-frontend: - labels: - - 'traefik.enable=true' - - 'traefik.http.routers.nomos-prod.rule=Host(`membership.vanhack.ca`)' - - 'traefik.http.routers.nomos-prod.entryPoints=websecure' - - 'traefik.http.routers.nomos-prod.tls=true' - - 'traefik.http.routers.nomos-prod.tls.certresolver=lets-encrypt' - - 'traefik.port=80' - - 'traefik.docker.network=proxy' diff --git a/docker-compose/webhooker-build.yml b/docker-compose/webhooker-build.yml deleted file mode 100644 index 0d9c5687b..000000000 --- a/docker-compose/webhooker-build.yml +++ /dev/null @@ -1,7 +0,0 @@ -services: - nomos-webhooker: - build: - context: .. - dockerfile: docker-compose/Dockerfile - target: webhooker - env_file: ../docker/nomos.env diff --git a/docker-compose/webhooker-logs-local.yml b/docker-compose/webhooker-logs-local.yml deleted file mode 100644 index 7ed10d689..000000000 --- a/docker-compose/webhooker-logs-local.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - nomos-webhooker: - volumes: - - ../logs:/app/logs diff --git a/docker-compose/webhooker-network-bridge.yml b/docker-compose/webhooker-network-bridge.yml deleted file mode 100644 index f73b75505..000000000 --- a/docker-compose/webhooker-network-bridge.yml +++ /dev/null @@ -1,3 +0,0 @@ -services: - nomos-webhooker: - network_mode: bridge diff --git a/docker-compose/webhooker-network-proxy.yml b/docker-compose/webhooker-network-proxy.yml deleted file mode 100644 index 4958ce788..000000000 --- a/docker-compose/webhooker-network-proxy.yml +++ /dev/null @@ -1,8 +0,0 @@ -services: - nomos-webhooker: - networks: - - proxy - -networks: - proxy: - external: true diff --git a/docker-compose/webhooker-network-rabbitmq.yml b/docker-compose/webhooker-network-rabbitmq.yml deleted file mode 100644 index a2ed7ab27..000000000 --- a/docker-compose/webhooker-network-rabbitmq.yml +++ /dev/null @@ -1,12 +0,0 @@ -services: - nomos-frontend: - networks: - - rabbitmq - - nomos-webhooker: - networks: - - rabbitmq - -networks: - rabbitmq: - external: true diff --git a/docker-compose/webhooker.yml b/docker-compose/webhooker.yml deleted file mode 100644 index 5515d6ff0..000000000 --- a/docker-compose/webhooker.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - nomos-webhooker: - image: vanhack/nomos-webhooker - container_name: nomos-webhooker - restart: always - - nomos-backend: - depends_on: - - nomos-webhooker diff --git a/docker/docker_compose_run.sh b/docker/docker_compose_run.sh deleted file mode 100755 index 652b71117..000000000 --- a/docker/docker_compose_run.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -docker_env_config.sh > /var/www/html/conf/env.php - -(cd /var/www/html/tools && php migrate.php -b -m -t) - -exec /usr/local/bin/docker-php-entrypoint "$@" diff --git a/docker/docker_env_config.sh b/docker/docker_env_config.sh deleted file mode 100755 index 915437188..000000000 --- a/docker/docker_env_config.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -echo "&2 'ERROR: Invalid installer checksum' - rm "${TMPFILE}" - exit 1 + if [ "${PHP_FILES}" != "" ]; then + FILES=$(echo "${PHP_FILES}" | xargs) pnpm exec just format php + pnpm exec just test php fi - php "${TMPFILE}" --install-dir ./tools --quiet - rm "${TMPFILE}" - else - echo "composer has already been set up!" - fi + if [ "${STORYBOOK_FILES}" != "" ]; then + pnpm --filter @vhs/nomos-frontend-react fix:storybook:titles + fi + + if [ "${VALIDATOR_FILES}" != "" ]; then + pnpm --filter @vhs/nomos-frontend-react generate:validator:implementations + fi + + FILES=$(echo "${FILES}" | xargs) pnpm exec just format all - ./tools/composer.sh install + git update-index --again -install_angular_ui_bootstrap: + exit 0 + fi + +git_hook_pre_push: #!/usr/bin/env bash - set -euo pipefail - mkdir -p web/components/custom/angular-ui/ \ - && cd web/components/custom/angular-ui/ \ - && wget https://raw.githubusercontent.com/angular-ui/bootstrap/refs/heads/gh-pages/ui-bootstrap-tpls-0.12.0.js \ - && wget https://raw.githubusercontent.com/angular-ui/bootstrap/refs/heads/gh-pages/ui-bootstrap-tpls-0.12.0.min.js + set -e -install_webcomponents: install_angular_ui_bootstrap + if git show-ref "$(git branch --show-current)" | awk '{ print $2 }' | xargs | sed 's/ /../g' | xargs git diff --numstat | grep packages/frontend-react > /dev/null; then + pnpm --filter @vhs/nomos-frontend-react build + fi -make_webcomponents_directories: - mkdir -p web/components/bower - mkdir -p web/components/custom + exit 0 -run_bower: - echo "Running bower" - ./tools/bower.sh install +install target: + @echo 'Installing {{target}}…' + @just "install_{{target}}" -run_composer: - echo "Running composer" - ./tools/composer.sh install +prepare: setup_husky setup target: @echo 'Setting up {{target}}…' - just "setup_{{target}}" - -setup_webcomponents: make_webcomponents_directories install_webcomponents run_bower + @just "setup_{{target}}" setup_husky: #!/usr/bin/env bash @@ -80,32 +87,21 @@ setup_husky: if [ ! -d .husky/_/ ]; then node_modules/.bin/husky + elif [ "$(grep 'hooksPath = .husky/_' .git/config)" = "" ] ; then + node_modules/.bin/husky else echo "husky has already been set up!" fi -setup_vendor: install_composer run_composer - -setup_webhooker: - #!/usr/bin/env bash - set -euo pipefail - - cd webhooker/ && npm install - test target: @echo 'Testing {{target}}…' - just "test_{{target}}" - -test_php: - #!/usr/bin/env bash - set -eo pipefail + @just "test_{{target}}" - vendor/bin/phpunit ${FILES:-app/ tests/ tools/ vhs/} +test_all: + @echo "Testing all packages..." + @pnpm --filter "./packages/*/" test -test_webhooker: - #!/usr/bin/env bash - set -eo pipefail +test_php: + @echo "Testing all php packages..." + @pnpm --filter "./packages/*-php/" test:php - if [ "${FILES}" != "" ] ; then - cd webhooker && npm run test - fi diff --git a/nomos.d.ts b/nomos.d.ts deleted file mode 100644 index 54d886e4f..000000000 --- a/nomos.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -import angular from '@types/angular' - -import moment from './web/components/bower/moment/moment' - -type CouldBeBoth = T | T[] - -declare global { - var moment: moment.fn - var $timeout: angular.ITimeoutService - - interface Date { - format: (format: string) => string - shortMonths: 'Jan' | 'Feb' | 'Mar' | 'Apr' | 'May' | 'Jun' | 'Jul' | 'Aug' | 'Sep' | 'Oct' | 'Nov' | 'Dec' - longMonths: - | 'January' - | 'February' - | 'March' - | 'April' - | 'May' - | 'June' - | 'July' - | 'August' - | 'September' - | 'October' - | 'November' - | 'December' - shortDays: 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' - longDays: 'Sunday' | 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' - } - - type HighchartsOptions = string | Record - - interface Highcharts { - chart: (element: HTMLElement, options?: HighchartsOptions) => void - getOptions: () => CouldBeBoth | Array> - } - - var Highcharts: Highcharts -} - -declare module 'angular' { - interface IScope { - options?: HighchartsOptions - } -} diff --git a/package.json b/package.json index ffb34e459..705470f23 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,20 @@ { "author": "", "dependencies": { - "@types/lodash": "^4.17.13", - "just-install": "^2.0.2", - "wireit": "^0.14.9" + "just-install": "^2.0.2" }, "description": "NOMOS Our Membership Operations Software. In greek mythology, Nomos is the personified spirit of law.", "devDependencies": { "@prettier/plugin-php": "^0.22.2", "@prettier/plugin-xml": "^3.4.1", - "@tyisi/config-eslint": "^4.0.0", "@tyisi/config-prettier": "^1.0.1", - "@types/angular": "^1.8.9", - "@types/bootstrap": "^5.2.10", - "@types/jquery": "^3.5.32", - "bower": "^1.8.14", - "eslint": "^9.15.0", "husky": "^9.1.7", "prettier": "^3.0.3", + "prettier-plugin-ini": "^1.3.0", + "prettier-plugin-nginx": "^1.0.3", "prettier-plugin-sh": "^0.14.0", "prettier-plugin-sql": "^0.18.1", - "typescript": "^5.7.2" + "prettier-plugin-tailwindcss": "^0.6.11" }, "directories": { "test": "tests" @@ -28,45 +22,26 @@ "keywords": [], "license": "ISC", "name": "@vhs/nomos", - "packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf", + "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977", "private": true, "scripts": { - "prepare": "wireit", - "test": "wireit" + "prepare": "just prepare", + "build": "sudo docker compose --env-file=docker-compose/nomos.env up --build", + "start": "sudo docker compose --env-file=docker-compose/nomos.env up", + "start:db": "sudo docker compose --env-file=docker-compose/nomos.env up db-mariadb", + "start:backend": "sudo docker compose --env-file=docker-compose/nomos.env up backend-php", + "start:frontend-web": "sudo docker compose --env-file=docker-compose/nomos.env up frontend-web", + "start:frontend-react": "sudo docker compose --env-file=docker-compose/nomos.env up frontend-react" }, "version": "1.0.0", - "wireit": { - "prepare": { - "dependencies": [ - "prepare:webcomponents", - "prepare:husky", - "prepare:vendor", - "prepare:webhooker" - ] - }, - "prepare:husky": { - "command": "just setup husky" - }, - "prepare:vendor": { - "command": "just setup vendor" - }, - "prepare:webcomponents": { - "command": "just setup webcomponents" - }, - "prepare:webhooker": { - "command": "just setup webhooker" - }, - "test": { - "dependencies": [ - "test:php", - "test:webhooker" - ] - }, - "test:php": { - "command": "just test php" - }, - "test:webhooker": { - "command": "just test php" - } + "pnpm": { + "onlyBuiltDependencies": [ + "dtrace-provider", + "just-install", + "@parcel/watcher", + "@swc/core", + "esbuild", + "msw" + ] } } diff --git a/.php-cs-fixer.php b/packages/backend-php/.php-cs-fixer.php similarity index 85% rename from .php-cs-fixer.php rename to packages/backend-php/.php-cs-fixer.php index be20281c5..025a68737 100644 --- a/.php-cs-fixer.php +++ b/packages/backend-php/.php-cs-fixer.php @@ -25,7 +25,7 @@ ] ], // 'blank_line_after_opening_tag' => true, - // 'blank_line_before_statement' => true, + 'blank_line_before_statement' => true, 'braces' => [ 'allow_single_line_closure' => true ], @@ -46,7 +46,8 @@ 'new_with_braces' => false, 'no_blank_lines_after_class_opening' => true, 'no_blank_lines_after_phpdoc' => true, - // "no_blank_lines_before_namespace" => true, + 'blank_lines_before_namespace' => true, + 'no_blank_lines_before_namespace' => false, 'no_empty_comment' => true, 'no_empty_phpdoc' => true, // 'no_empty_statement' => true, @@ -69,30 +70,13 @@ 'no_spaces_around_offset' => true, // 'no_trailing_comma_in_list_call' => true, // 'no_trailing_comma_in_singleline_array' => true, - // 'no_unneeded_control_parentheses' => true, - // 'no_unused_imports' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unused_imports' => true, 'no_whitespace_before_comma_in_array' => true, 'no_whitespace_in_blank_line' => true, // 'normalize_index_brace' => true, 'object_operator_without_whitespace' => true, 'php_unit_fqcn_annotation' => true, - 'phpdoc_align' => true, - 'phpdoc_annotation_without_dot' => true, - 'phpdoc_indent' => true, - // 'phpdoc_no_access' => true, - // 'phpdoc_no_alias_tag' => true, - // 'phpdoc_no_empty_return' => true, - // 'phpdoc_no_package' => true, - // 'phpdoc_no_useless_inheritdoc' => true, - // 'phpdoc_return_self_reference' => true, - // 'phpdoc_scalar' => true, - 'phpdoc_separation' => true, - 'phpdoc_single_line_var_spacing' => true, - 'phpdoc_summary' => true, - // 'phpdoc_to_comment' => true, - 'phpdoc_trim' => true, - 'phpdoc_types' => true, - // 'phpdoc_var_without_name' => true, // 'increment_style' => true, // 'return_type_declaration' => true, // 'self_accessor' => true, @@ -106,7 +90,6 @@ 'trim_array_spaces' => true, 'unary_operator_spaces' => true, 'whitespace_after_comma_in_array' => true, - 'space_after_semicolon' => true, 'single_blank_line_at_eof' => true, 'ordered_class_elements' => [ 'order' => [ @@ -165,7 +148,34 @@ ], 'sort_algorithm' => 'alpha' ], - 'ordered_imports' => true + 'ordered_imports' => true, + 'phpdoc_order_by_value' => true, + 'phpdoc_types_order' => ['sort_algorithm' => 'alpha', 'null_adjustment' => 'always_last'], + 'phpdoc_add_missing_param_annotation' => true, + 'phpdoc_var_annotation_correct_order' => true, + 'phpdoc_align' => true, + 'phpdoc_annotation_without_dot' => true, + 'phpdoc_indent' => true, + // 'phpdoc_no_access' => true, + // 'phpdoc_no_alias_tag' => true, + 'phpdoc_no_empty_return' => false, + // 'phpdoc_no_package' => true, + // 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_return_self_reference' => true, + 'phpdoc_scalar' => true, + 'phpdoc_separation' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_summary' => true, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_line_span' => ['property' => 'single'], + 'phpdoc_order' => true, + 'phpdoc_param_order' => true, + 'phpdoc_to_comment' => false, + 'phpdoc_trim_consecutive_blank_line_separation' => true, + 'no_superfluous_phpdoc_tags' => false + // 'void_return' => true, + // 'phpdoc_var_without_name' => true, ]) ->setIndent(' ') ->setLineEnding("\n") diff --git a/packages/backend-php/app/adapters/v2/EmailAdapter2.php b/packages/backend-php/app/adapters/v2/EmailAdapter2.php new file mode 100644 index 000000000..535e6e5d6 --- /dev/null +++ b/packages/backend-php/app/adapters/v2/EmailAdapter2.php @@ -0,0 +1,70 @@ +subject; + } + + return \vhs\gateways\Engine::getInstance() + ->getDefaultGateway('messages', 'email') + ->sendRichEmail($recipients, $subject, $generated->txt, $generated->html); + } + + /** + * Send the email. + * + * @param mixed $template + * @param mixed $context + * @param string|null $subject + * + * @throws \app\exceptions\InvalidInputException + * + * @return bool + */ + public function EmailUser(User $user, $template, $context, $subject = null): bool { + $generated = EmailTemplate::generate($template, $context); + + if (is_null($generated)) { + throw new InvalidInputException('Unable to load e-mail template'); + } + + if (is_null($subject)) { + $subject = $generated->subject; + } + + return \vhs\gateways\Engine::getInstance() + ->getDefaultGateway('messages', 'email') + ->sendRichEmail($user->email, $subject, $generated->txt, $generated->html); + } +} diff --git a/app/app.php b/packages/backend-php/app/app.php similarity index 88% rename from app/app.php rename to packages/backend-php/app/app.php index e639f1f15..6e7e2fed4 100644 --- a/app/app.php +++ b/packages/backend-php/app/app.php @@ -10,14 +10,14 @@ require_once '../conf/config.ini.php'; //Debug defined in /conf/config.ini.php -if (DEBUG) { +if (defined('DEBUG')) { error_reporting(E_ALL); ini_set('display_errors', 1); } require_once 'include.php'; -$serverLog = new \vhs\loggers\FileLogger(dirname(__FILE__) . '/../logs/server.log'); +$serverLog = new \vhs\loggers\FileLogger(\vhs\BasePath::getBasePath(false) . '/logs/server.log'); \vhs\web\HttpContext::Init(new \vhs\web\HttpServer(new \vhs\web\modules\HttpServerInfoModule('Nomos'), $serverLog)); @@ -27,6 +27,7 @@ \vhs\web\HttpContext::Server()->register(new \vhs\web\modules\HttpExceptionHandlerModule('verbose', $serverLog)); \vhs\web\HttpContext::Server()->register(\app\modules\HttpPaymentGatewayHandlerModule::getInstance()); \vhs\web\HttpContext::Server()->register(\app\security\oauth\modules\OAuthHandlerModule::getInstance()); +\vhs\web\HttpContext::Server()->register(new \vhs\web\modules\HttpJsonServiceHandlerModule('v2')); \vhs\web\HttpContext::Server()->register(new \vhs\web\modules\HttpJsonServiceHandlerModule('web')); \app\modules\HttpPaymentGatewayHandlerModule::register(new \app\gateways\PaypalGateway()); @@ -36,10 +37,11 @@ \app\monitors\PaypalIpnMonitor::getInstance()->Init($serverLog); \app\monitors\StripeEventMonitor::getInstance()->Init($serverLog); \app\monitors\PaymentMonitor::getInstance()->Init($serverLog); -\app\monitors\DomainEventMonitor::getInstance()->Init($serverLog); \app\security\oauth\modules\OAuthHandlerModule::register(new \app\security\oauth\modules\GithubOAuthHandler()); \app\security\oauth\modules\OAuthHandlerModule::register(new \app\security\oauth\modules\GoogleOAuthHandler()); \app\security\oauth\modules\OAuthHandlerModule::register(new \app\security\oauth\modules\SlackOAuthHandler()); +\vhs\gateways\Engine::getInstance()->setLogger($serverLog); + \vhs\web\HttpContext::Server()->handle(); diff --git a/packages/backend-php/app/constants/Errors.php b/packages/backend-php/app/constants/Errors.php new file mode 100644 index 000000000..131c0c542 --- /dev/null +++ b/packages/backend-php/app/constants/Errors.php @@ -0,0 +1,11 @@ + $context + * @param string|null $subject + * + * @return void + */ + public function Email($email, $tmpl, $context, $subject = null): void; + + /** + * Summary of EmailUser. + * + * @permission administrator + * + * @param \app\domain\User $user email address + * @param string $tmpl + * @param array $context + * @param string|null $subject + * + * @return void + */ + public function EmailUser($user, $tmpl, $context, $subject = null): void; + + /** + * @permission administrator + * + * @param int $id + * + * @return \app\domain\EmailTemplate|null + */ + public function GetTemplate($id): EmailTemplate|null; + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\EmailTemplate[] + */ + public function ListTemplates($page, $size, $columns, $order, $filters): array; + + /** + * @permission administrator + * + * @param string $name + * @param string $code + * @param string $subject + * @param string $help + * @param string $body + * @param string $html + * + * @return bool + */ + public function PutTemplate($name, $code, $subject, $help, $body, $html): bool; + + /** + * @permission administrator + * + * @param int $id + * @param string $name + * @param string $code + * @param string $subject + * @param string $help + * @param string $body + * @param string $html + * + * @return bool + */ + public function UpdateTemplate($id, $name, $code, $subject, $help, $body, $html): bool; + + /** + * @permission administrator + * + * @param int $id + * @param string $body + * + * @return bool + */ + public function UpdateTemplateBody($id, $body): bool; + + /** + * @permission administrator + * + * @param int $id + * @param string $code + * + * @return bool + */ + public function UpdateTemplateCode($id, $code): bool; + + /** + * @permission administrator + * + * @param int $id + * @param string $help + * + * @return bool + */ + public function UpdateTemplateHelp($id, $help): bool; + + /** + * @permission administrator + * + * @param int $id + * @param string $html + * + * @return bool + */ + public function UpdateTemplateHtml($id, $html): bool; + + /** + * @permission administrator + * + * @param int $id + * @param string $name + * + * @return bool + */ + public function UpdateTemplateName($id, $name): bool; + + /** + * @permission administrator + * + * @param int $id + * @param string $subject + * + * @return bool + */ + public function UpdateTemplateSubject($id, $subject): bool; +} diff --git a/packages/backend-php/app/contracts/v2/IEventService2.php b/packages/backend-php/app/contracts/v2/IEventService2.php new file mode 100644 index 000000000..766269962 --- /dev/null +++ b/packages/backend-php/app/contracts/v2/IEventService2.php @@ -0,0 +1,140 @@ +|array + */ + public function GetUserGrantablePrivileges($userid): array; + + /** + * @permission administrator + * + * @return \app\domain\User[] + */ + public function GetUsers(): array; + + /** + * @permission grants + * + * @param int $userid + * @param string $privilege + * + * @return bool + */ + public function GrantPrivilege($userid, $privilege): bool; + + /** + * @permission administrator|grants + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\User[] + */ + public function ListUsers($page, $size, $columns, $order, $filters): array; + + /** + * @permission administrator + * + * @param int $userid + * @param string|string[] $privileges + * + * @return bool + */ + public function PutUserPrivileges($userid, $privileges): bool; + + /** + * @permission anonymous + * + * @param string $username + * @param string $password + * @param string $email + * @param string $fname + * @param string $lname + * + * @return \app\domain\User + */ + public function Register($username, $password, $email, $fname, $lname): User; + + /** + * @permission anonymous + * + * @param string $email + * + * @return \app\dto\ServiceResponseError|\app\dto\ServiceResponseSuccess + */ + public function RequestPasswordReset($email): ServiceResponseSuccess|ServiceResponseError; + + /** + * @permission user + * + * @param string $email + * + * @return bool|string|null + */ + public function RequestSlackInvite($email): bool|string|null; + + /** + * @permission anonymous + * + * @param string $token + * @param string $password + * + * @return \app\dto\ServiceResponseError|\app\dto\ServiceResponseSuccess + */ + public function ResetPassword($token, $password): ServiceResponseSuccess|ServiceResponseError; + + /** + * @permission grants + * + * @param int $userid + * @param string $privilege + * + * @return bool + */ + public function RevokePrivilege($userid, $privilege): bool; + + /** + * @permission administrator + * + * @param int $userid + * @param bool|string $cash + * + * @return bool + */ + public function UpdateCash($userid, $cash): bool; + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $email + * + * @return bool + */ + public function UpdateEmail($userid, $email): bool; + + /** + * @permission administrator + * + * @param int $userid + * @param string $date + * + * @return bool + */ + public function UpdateExpiry($userid, $date): bool; + + /** + * @permission administrator + * + * @param int $userid + * @param int $membershipid + * + * @return bool + */ + public function UpdateMembership($userid, $membershipid): bool; + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $fname + * @param string $lname + * + * @return bool + */ + public function UpdateName($userid, $fname, $lname): bool; + + /** + * @permission administrator|user + * + * @param int $userid + * @param bool $subscribe + * + * @return bool + */ + public function UpdateNewsletter($userid, $subscribe): bool; + + /** + * @permission administrator|user + * + * @param int $userid + * @param string $password + * + * @return bool + */ + public function UpdatePassword($userid, $password): bool; + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $email + * + * @return bool + */ + public function UpdatePaymentEmail($userid, $email): bool; + + /** + * @permission administrator + * + * @param int $userid + * @param string $status + * + * @return bool + */ + public function UpdateStatus($userid, $status): bool; + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $email + * + * @return bool + */ + public function UpdateStripeEmail($userid, $email): bool; + + /** + * @permission administrator|user + * + * @param int $userid + * @param string $username + * + * @return bool + */ + public function UpdateUsername($userid, $username): bool; +} diff --git a/packages/backend-php/app/contracts/v2/IWebHookService2.php b/packages/backend-php/app/contracts/v2/IWebHookService2.php new file mode 100644 index 000000000..1d900e2de --- /dev/null +++ b/packages/backend-php/app/contracts/v2/IWebHookService2.php @@ -0,0 +1,152 @@ + + * + * @typescript + */ class AccessLog extends Domain { - public static function Define() { + /** + * Define. + * + * @return void + */ + public static function Define(): void { AccessLog::Schema(AccessLogSchema::Type()); } + /** + * findLatest. + * + * @param int $limit + * + * @return AccessLog[] + */ public static function findLatest($limit = 5) { return self::where( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(AccessLogSchema::Columns()->authorized, false), + // TODO implement proper typing + // @phpstan-ignore property.notFound OrderBy::Descending(AccessLogSchema::Columns()->time), $limit ); } + /** + * log. + * + * @param string $key + * @param string $type + * @param bool $authorized + * @param string $from_ip + * @param int|null $userid + * + * @return AccessLog + */ public static function log($key, $type, $authorized, $from_ip, $userid = null) { $entry = new AccessLog(); + $entry->key = $key; $entry->type = $type; $entry->authorized = $authorized; diff --git a/app/domain/AccessToken.php b/packages/backend-php/app/domain/AccessToken.php similarity index 56% rename from app/domain/AccessToken.php rename to packages/backend-php/app/domain/AccessToken.php index d15dfb0b9..d8963e301 100644 --- a/app/domain/AccessToken.php +++ b/packages/backend-php/app/domain/AccessToken.php @@ -15,16 +15,47 @@ use vhs\domain\Domain; use vhs\domain\validations\ValidationResults; +/** + * AccessToken domain implementation. + * + * @property int $id + * @property string $token + * @property \DateTime|string $expires + * @property int $userid + * @property int $appclientid + * @property \app\domain\User $user + * @property \app\domain\AppClient $client + * + * @extends Domain + * + * @typescript + */ class AccessToken extends Domain { - public static function Define() { + /** + * Define. + * + * @return void + */ + public static function Define(): void { AccessToken::Schema(AccessTokenSchema::Type()); AccessToken::Relationship('user', User::Type()); AccessToken::Relationship('client', AppClient::Type()); } + /** + * findByToken. + * + * @param string $token + * + * @return AccessToken|null + */ public static function findByToken($token) { $tokens = self::where( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(AccessTokenSchema::Columns()->token, $token), + // TODO implement proper typing + // @phpstan-ignore property.notFound OrderBy::Descending(AccessTokenSchema::Columns()->expires), 1 ); diff --git a/packages/backend-php/app/domain/AppClient.php b/packages/backend-php/app/domain/AppClient.php new file mode 100644 index 000000000..87dc1ab45 --- /dev/null +++ b/packages/backend-php/app/domain/AppClient.php @@ -0,0 +1,55 @@ + + * + * @typescript + */ +class AppClient extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + AppClient::Schema(AppClientSchema::Type()); + AppClient::Relationship('owner', User::Type()); + } + + /** + * @param ValidationResults $results + * + * @return bool + */ + public function validate(ValidationResults &$results) { + return true; + } +} diff --git a/packages/backend-php/app/domain/EmailTemplate.php b/packages/backend-php/app/domain/EmailTemplate.php new file mode 100644 index 000000000..423680cba --- /dev/null +++ b/packages/backend-php/app/domain/EmailTemplate.php @@ -0,0 +1,96 @@ + + * + * @typescript + */ +class EmailTemplate extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + EmailTemplate::Schema(EmailSchema::Type()); + } + + /** + * findByCode. + * + * @param string $code + * + * @return EmailTemplate|null + */ + public static function findByCode($code) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + $val = EmailTemplate::where(Where::Equal(EmailSchema::Columns()->code, $code)); + + if (!empty($val)) { + return $val[0]; + } + + return null; + } + + /** + * generate. + * + * @param string $code + * @param array|string $context + * + * @return \app\dto\GeneratedEmailResults|null + */ + public static function generate($code, $context) { + $template = self::findByCode($code); + + if (is_null($template)) { + return null; + } + + $engine = new \StringTemplate\Engine('{{', '}}'); + + $ret = new GeneratedEmailResults(); + + $ret->subject = $engine->render($template->subject, $context); + $ret->txt = $engine->render($template->body, $context); + $ret->html = $engine->render($template->html, $context); + + return $ret; + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/Event.php b/packages/backend-php/app/domain/Event.php new file mode 100644 index 000000000..10db5393b --- /dev/null +++ b/packages/backend-php/app/domain/Event.php @@ -0,0 +1,76 @@ + + * + * @typescript + */ +class Event extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + Event::Schema(EventSchema::Type()); + + Event::Relationship('privileges', Privilege::Type(), EventPrivilegeSchema::Type()); + } + + /** + * exists. + * + * @param mixed $domain + * @param mixed $event + * + * @return bool + */ + public static function exists($domain, $event) { + $events = Event::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Event::Schema()->Columns()->domain, $domain), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Event::Schema()->Columns()->event, $event) + ) + ); + + return count($events) > 0; + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/GenuineCard.php b/packages/backend-php/app/domain/GenuineCard.php new file mode 100644 index 000000000..ea9b3b318 --- /dev/null +++ b/packages/backend-php/app/domain/GenuineCard.php @@ -0,0 +1,63 @@ + + * + * @typescript + */ +class GenuineCard extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + GenuineCard::Schema(GenuineCardSchema::Type()); + } + + /** + * @param string $key + * + * @return GenuineCard[] + */ + public static function findByKey($key) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return GenuineCard::where(Where::Equal(GenuineCardSchema::Columns()->key, $key)); + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/Ipn.php b/packages/backend-php/app/domain/Ipn.php new file mode 100644 index 000000000..28931fc6b --- /dev/null +++ b/packages/backend-php/app/domain/Ipn.php @@ -0,0 +1,52 @@ + + * + * @typescript + */ +class Ipn extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + Ipn::Schema(IpnSchema::Type()); + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/Key.php b/packages/backend-php/app/domain/Key.php new file mode 100644 index 000000000..81a1be87a --- /dev/null +++ b/packages/backend-php/app/domain/Key.php @@ -0,0 +1,254 @@ + + * + * @typescript + */ +class Key extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + Key::Schema(KeySchema::Type()); + + Key::Relationship('privileges', Privilege::Type(), KeyPrivilegeSchema::Type()); + } + + /** + * find key by api key. + * + * @param string $key + * + * @return \app\domain\Key[] + */ + public static function findByApiKey($key) { + return self::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->type, 'api'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->key, $key) + ) + ); + } + + /** + * find key by pin key. + * + * @param string $pin + * + * @return Key[]|null + */ + public static function findByPin($pin) { + return self::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->type, 'pin'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->key, $pin) + ) + ); + } + + /** + * find key by rfid key. + * + * @param string $rfid + * + * @return Key[]|null + */ + public static function findByRfid($rfid) { + return self::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->type, 'rfid'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->key, $rfid) + ) + ); + } + + /** + * find key by service key. + * + * @param string $service + * @param string $key + * + * @return Key[]|null + */ + public static function findByService($service, $key) { + return self::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->type, $service), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->key, $key) + ) + ); + } + + /** + * find keys by types. + * + * @param string ...$types + * + * @return Key[]|null + */ + public static function findByTypes(...$types) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return self::where(Where::In(KeySchema::Columns()->type, $types)); + } + + /** + * find by key and type. + * + * @param string $key + * @param string $type + * + * @return Key[]|null + */ + public static function findKeyAndType($key, $type) { + return self::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->type, $type), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->key, $key) + ) + ); + } + + /** + * get system (non-owned) api keys. + * + * @return Key[]|null + */ + public static function getSystemApiKeys() { + return self::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Null(KeySchema::Columns()->userid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->type, 'api') + ) + ); + } + + /** + * get api keys for a particular user id. + * + * @param int $userid + * + * @return Key[]|null + */ + public static function getUserApiKeys($userid) { + return self::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Key::Schema()->Columns()->type, 'api'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Key::Schema()->Columns()->userid, $userid) + ) + ); + } + + /** + * getAbsolutePrivileges. + * + * @return mixed + */ + public function getAbsolutePrivileges() { + $privs = []; + + // TODO fix typing + /** @disregard P1006 PHP0404 override */ + foreach ($this->privileges->all() as $priv) { + if ($priv->code === 'inherit' && $this->userid != null) { + $user = User::find($this->userid); + + if ($user != null) { + // TODO fix typing + /** @disregard P1006 override */ + foreach ($user->privileges->all() as $userpriv) { + array_push($privs, $userpriv); + } + + if (!is_null($user->membership)) { + // TODO fix typing + /** @disregard P1006 PHP0404 override */ + foreach ($user->membership->privileges->all() as $mempriv) { + array_push($privs, $mempriv); + } + } + } + } + + array_push($privs, $priv); + } + + $retval = []; + + foreach (array_unique($privs) as $priv) { + //hack array_unique may convert to object + array_push($retval, $priv); + } + + return $retval; + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/app/domain/Membership.php b/packages/backend-php/app/domain/Membership.php similarity index 61% rename from app/domain/Membership.php rename to packages/backend-php/app/domain/Membership.php index de74fcac5..a7bb1ce40 100644 --- a/app/domain/Membership.php +++ b/packages/backend-php/app/domain/Membership.php @@ -19,6 +19,24 @@ use vhs\domain\Domain; use vhs\domain\validations\ValidationResults; +/** + * @property int $id + * @property string $title + * @property string $code + * @property string $description + * @property float $price + * @property int $days + * @property string $period + * @property bool $trial + * @property bool $recurring + * @property bool $private + * @property bool $active + * @property object $privileges + * + * @extends Domain + * + * @typescript + */ class Membership extends Domain { public const FRIEND = 'vhs_membership_friend'; /* TODO HACK we should instead add privileges to the membership types and check those instead when checking types @@ -29,6 +47,11 @@ class Membership extends Domain { public const KEYHOLDER = 'vhs_membership_keyholder'; public const MEMBER = 'vhs_membership_member'; + /** + * Get a map of all code IDs. + * + * @return array + */ public static function allCodeIdMap() { $rows = Database::select( Query::Select(MembershipSchema::Table(), new Columns(MembershipSchema::Column('id'), MembershipSchema::Column('code'))) @@ -43,6 +66,11 @@ public static function allCodeIdMap() { return $values; } + /** + * Undocumented function. + * + * @return string[] + */ public static function allCodes() { $rows = Database::select(Query::Select(MembershipSchema::Table(), new Columns(MembershipSchema::Column('code')))); @@ -55,29 +83,47 @@ public static function allCodes() { return $values; } - public static function Define() { + /** + * Define. + * + * @return void + */ + public static function Define(): void { Membership::Schema(MembershipSchema::Type()); Membership::Relationship('privileges', Privilege::Type(), MembershipPrivilegeSchema::Type()); } /** - * @param $code + * Find membership by code. + * + * @param string $code * * @return Membership[] */ public static function findByCode($code) { + // TODO implement proper typing + // @phpstan-ignore property.notFound return Membership::where(Where::Equal(MembershipSchema::Columns()->code, $code)); } /** - * @param $price + * @param float $price * - * @return Membership + * @return Membership|null */ public static function findForPriceLevel($price) { $memberships = Membership::where( - Where::_And(Where::LesserEqual(MembershipSchema::Columns()->price, $price), Where::Equal(MembershipSchema::Columns()->active, true)), + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::LesserEqual(MembershipSchema::Columns()->price, $price), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(MembershipSchema::Columns()->active, true) + ), + // TODO implement proper typing + // @phpstan-ignore property.notFound OrderBy::Descending(MembershipSchema::Columns()->price), 1 ); @@ -89,7 +135,14 @@ public static function findForPriceLevel($price) { return null; } + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ public function validate(ValidationResults &$results) { - // not required at this time + // TODO: Implement validate() method. } } diff --git a/packages/backend-php/app/domain/PasswordResetRequest.php b/packages/backend-php/app/domain/PasswordResetRequest.php new file mode 100644 index 000000000..51049fade --- /dev/null +++ b/packages/backend-php/app/domain/PasswordResetRequest.php @@ -0,0 +1,60 @@ + + * + * @typescript + */ +class PasswordResetRequest extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + PasswordResetRequest::Schema(PasswordResetRequestSchema::Type()); + } + + /** + * findByToken. + * + * @param string $token + * + * @return PasswordResetRequest[]|null + */ + public static function findByToken($token) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return PasswordResetRequest::where(Where::Equal(PasswordResetRequestSchema::Columns()->token, $token)); + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/Payment.php b/packages/backend-php/app/domain/Payment.php new file mode 100644 index 000000000..9ed9315b8 --- /dev/null +++ b/packages/backend-php/app/domain/Payment.php @@ -0,0 +1,79 @@ + + * + * @typescript + */ +class Payment extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + Payment::Schema(PaymentSchema::Type()); + } + + /** + * exists. + * + * @param string $txn_id + * + * @return bool + */ + public static function exists($txn_id) { + return Database::exists( + Query::select( + PaymentSchema::Table(), + PaymentSchema::Columns(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(PaymentSchema::Columns()->txn_id, $txn_id) + ) + ); + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/app/domain/Privilege.php b/packages/backend-php/app/domain/Privilege.php similarity index 52% rename from app/domain/Privilege.php rename to packages/backend-php/app/domain/Privilege.php index 211103474..a9c59714a 100644 --- a/app/domain/Privilege.php +++ b/packages/backend-php/app/domain/Privilege.php @@ -16,16 +16,46 @@ use vhs\security\CurrentUser; use vhs\security\exceptions\UnauthorizedException; +/** + * Privilege domain implementation. + * + * @property int $id + * @property string $name + * @property string $code + * @property string $description + * @property string $icon + * @property bool $enabled + * + * @extends Domain + * + * @typescript + */ class Privilege extends Domain { - public static function Define() { + /** + * Define. + * + * @return void + */ + public static function Define(): void { Privilege::Schema(PrivilegeSchema::Type()); } + /** + * findByCode. + * + * @param string $code + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Privilege|null + */ public static function findByCode($code) { if (!self::checkCodeAccess($code)) { throw new UnauthorizedException(); } + // TODO implement proper typing + // @phpstan-ignore property.notFound $privs = Privilege::where(Where::Equal(Privilege::Schema()->Columns()->code, $code)); if (!empty($privs)) { @@ -35,15 +65,33 @@ public static function findByCode($code) { return null; } - public static function findByCodes(...$codes) { + /** + * findByCodes. + * + * @param string ...$codes + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Privilege[]|null + */ + public static function findByCodes(string ...$codes) { if (!self::checkCodeAccess(...$codes)) { throw new UnauthorizedException(); } + // TODO implement proper typing + // @phpstan-ignore property.notFound return Privilege::where(Where::In(Privilege::Schema()->Columns()->code, $codes)); } - private static function checkCodeAccess(...$codes) { + /** + * checkCodeAccess. + * + * @param string ...$codes + * + * @return bool + */ + private static function checkCodeAccess(string ...$codes) { foreach ($codes as $code) { if ( $code != 'inherit' && @@ -58,7 +106,14 @@ private static function checkCodeAccess(...$codes) { return true; } + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ public function validate(ValidationResults &$results) { - // Do nothing + // TODO: Implement validate() method. } } diff --git a/app/domain/RefreshToken.php b/packages/backend-php/app/domain/RefreshToken.php similarity index 53% rename from app/domain/RefreshToken.php rename to packages/backend-php/app/domain/RefreshToken.php index 74c326c03..ef2311c6f 100644 --- a/app/domain/RefreshToken.php +++ b/packages/backend-php/app/domain/RefreshToken.php @@ -15,16 +15,45 @@ use vhs\domain\Domain; use vhs\domain\validations\ValidationResults; +/** + * @property int $id + * @property string $token + * @property string $expires + * @property int $userid + * @property \app\domain\User $user + * @property int $appclientid + * @property \app\domain\AppClient $client + * + * @extends Domain + * + * @typescript + */ class RefreshToken extends Domain { - public static function Define() { + /** + * Define. + * + * @return void + */ + public static function Define(): void { RefreshToken::Schema(RefreshTokenSchema::Type()); RefreshToken::Relationship('user', User::Type()); RefreshToken::Relationship('client', AppClient::Type()); } + /** + * findByToken. + * + * @param string $token + * + * @return \app\domain\RefreshToken|null + */ public static function findByToken($token) { $tokens = self::where( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(RefreshTokenSchema::Columns()->token, $token), + // TODO implement proper typing + // @phpstan-ignore property.notFound OrderBy::Descending(RefreshTokenSchema::Columns()->expires), 1 ); @@ -37,7 +66,9 @@ public static function findByToken($token) { } /** - * @param ValidationResults $results + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results * * @return bool */ diff --git a/packages/backend-php/app/domain/StripeEvent.php b/packages/backend-php/app/domain/StripeEvent.php new file mode 100644 index 000000000..07fcf6f33 --- /dev/null +++ b/packages/backend-php/app/domain/StripeEvent.php @@ -0,0 +1,50 @@ + + * + * @typescript + */ +class StripeEvent extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + StripeEvent::Schema(StripeEventSchema::Type()); + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/app/domain/SystemPreference.php b/packages/backend-php/app/domain/SystemPreference.php similarity index 65% rename from app/domain/SystemPreference.php rename to packages/backend-php/app/domain/SystemPreference.php index 0924f74b8..1cab77290 100644 --- a/app/domain/SystemPreference.php +++ b/packages/backend-php/app/domain/SystemPreference.php @@ -15,20 +15,39 @@ use vhs\domain\Domain; use vhs\domain\validations\ValidationResults; +/** + * @property int $id + * @property string $key + * @property string $value + * @property bool $enabled + * @property string $notes + * @property object $privileges + * + * @extends Domain + * + * @typescript + */ class SystemPreference extends Domain { - public static function Define() { + /** + * Define. + * + * @return void + */ + public static function Define(): void { SystemPreference::Schema(SystemPreferenceSchema::Type()); SystemPreference::Relationship('privileges', Privilege::Type(), SystemPreferencePrivilegeSchema::Type()); } /** - * @param $key + * @param string $key * @param callable|null $accessCheck Privilege[] returns bool * - * @return array + * @return SystemPreference[] */ - public static function findByKey($key, callable $accessCheck = null) { + public static function findByKey($key, ?callable $accessCheck = null) { + // TODO implement proper typing + // @phpstan-ignore property.notFound $prefs = SystemPreference::where(Where::Equal(SystemPreferenceSchema::Columns()->key, $key)); if (is_null($prefs) || count($prefs) == 0 || is_null($accessCheck)) { @@ -48,9 +67,11 @@ public static function findByKey($key, callable $accessCheck = null) { } /** - * @param ValidationResults $results + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results * - * @return bool + * @return void */ public function validate(ValidationResults &$results) { // TODO: Implement validate() method. diff --git a/app/domain/User.php b/packages/backend-php/app/domain/User.php similarity index 57% rename from app/domain/User.php rename to packages/backend-php/app/domain/User.php index a957bc47f..d641a6db2 100644 --- a/app/domain/User.php +++ b/packages/backend-php/app/domain/User.php @@ -9,6 +9,7 @@ namespace app\domain; +use app\dto\UserActiveEnum; use app\schema\UserPrivilegeSchema; use app\schema\UserSchema; use DateTime; @@ -19,8 +20,49 @@ use vhs\domain\validations\ValidationFailure; use vhs\domain\validations\ValidationResults; +/** + * User domain implementation. + * + * @method static \app\domain\User|null find(int $primaryKeyValues) + * + * @property int $id + * @property string $username + * @property string $password + * @property int $membership_id + * @property \DateTime|string $mem_expire + * @property bool $trial_used + * @property string $email + * @property string $fname + * @property string $lname + * @property string|null $token + * @property string $cookie_id + * @property bool $newsletter + * @property bool $cash + * @property int $userlevel + * @property string $notes + * @property \DateTime|string $created + * @property \DateTime|string $lastlogin + * @property string $lastip + * @property string $avatar + * @property string $active + * @property string $paypal_id + * @property string $payment_email + * @property string $stripe_id + * @property string $stripe_email + * @property bool $valid + * @property object $membership + * @property object $privileges + * @property object $keys + * + * @extends Domain + * + * @typescript + */ class User extends Domain { - public static function Define() { + /** + * Define. + */ + public static function Define(): void { User::Schema(UserSchema::Type()); User::Relationship('keys', Key::Type()); User::Relationship('membership', Membership::Type()); @@ -31,10 +73,15 @@ public static function Define() { * @param string|null $username * @param string|null $email * - * @return boolean + * @return bool */ public static function exists($username = null, $email = null) { + // TODO implement proper typing + // @phpstan-ignore property.notFound $usernameWhere = Where::Equal(UserSchema::Columns()->username, $username); + + // TODO implement proper typing + // @phpstan-ignore property.notFound $emailWhere = Where::Equal(UserSchema::Columns()->email, $email); $where = null; @@ -50,44 +97,59 @@ public static function exists($username = null, $email = null) { } /** - * @param $email + * @param string $email * - * @return User[] + * @return User[]|null */ public static function findByEmail($email) { + // TODO implement proper typing + // @phpstan-ignore property.notFound return User::where(Where::Equal(UserSchema::Columns()->email, $email)); } /** - * @param $email + * @param string $email * - * @return User[] + * @return User[]|null */ public static function findByPaymentEmail($email) { + // TODO implement proper typing + // @phpstan-ignore property.notFound return User::where(Where::Equal(UserSchema::Columns()->payment_email, $email)); } + /** + * findByToken. + * + * @param mixed $token + * + * @return User[]|null + */ public static function findByToken($token) { + // TODO implement proper typing + // @phpstan-ignore property.notFound return User::where(Where::Equal(UserSchema::Columns()->token, $token)); } /** - * @param $username + * @param string $username * - * @return User[] + * @return User[]|null */ public static function findByUsername($username) { + // TODO implement proper typing + // @phpstan-ignore property.notFound return User::where(Where::Equal(UserSchema::Columns()->username, $username)); } /** * Magic field interface method for 'valid'. * - * @return boolean + * @return bool */ public function get_valid() { // Check if account is active - if ($this->active != 'y') { + if ($this->active != UserActiveEnum::ACTIVE->value) { return false; } @@ -103,8 +165,16 @@ public function get_valid() { return !$this->hasExpired(); } + /** + * getGrantCodes. + * + * @return array + */ public function getGrantCodes() { $grants = []; + + // TODO fix typing + /** @disregard P1006 override */ foreach ($this->privileges->all() as $priv) { if (strpos($priv->code, 'grant:') === 0) { array_push($grants, substr($priv->code, 6)); @@ -117,7 +187,7 @@ public function getGrantCodes() { /** * Get a friendly error message for user validity. * - * @return mixed + * @return 'Account expired'|'Account is not active'|'Unknown error'|false */ public function getInvalidReason() { if ($this->valid) { @@ -125,7 +195,7 @@ public function getInvalidReason() { } // Check if account is active - if ($this->active != 'y') { + if ($this->active != UserActiveEnum::ACTIVE->value) { return 'Account is not active'; } @@ -137,9 +207,16 @@ public function getInvalidReason() { return 'Unknown error'; } + /** + * getPrivilegeCodes. + * + * @return string[] + */ public function getPrivilegeCodes() { $codes = []; + // TODO fix typing + /** @disregard P1006 override */ foreach ($this->privileges->all() as $priv) { array_push($codes, $priv->code); } @@ -148,18 +225,28 @@ public function getPrivilegeCodes() { } public function validate(ValidationResults &$results) { - $this->validateEmail($results); + // TODO: Implement validate() method. } /** * Check if user account has expired. * - * @return boolean + * @return bool */ private function hasExpired() { return new DateTime($this->mem_expire) < new DateTime(); } + /** + * validateEmail. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + * + * @disregard + */ + // @phpstan-ignore method.unused private function validateEmail(ValidationResults &$results) { if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)) { $results->add(new ValidationFailure('Invalid e-mail address')); diff --git a/packages/backend-php/app/domain/WebHook.php b/packages/backend-php/app/domain/WebHook.php new file mode 100644 index 000000000..cc70e32dd --- /dev/null +++ b/packages/backend-php/app/domain/WebHook.php @@ -0,0 +1,80 @@ + + * + * @typescript + */ +class WebHook extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + WebHook::Schema(WebHookSchema::Type()); + + WebHook::Relationship('privileges', Privilege::Type(), WebHookPrivilegeSchema::Type()); + WebHook::Relationship('event', Event::Type()); + } + + /** + * findByDomainEvent. + * + * @param string $domain + * @param string $event + * + * @return WebHook[]|null + */ + public static function findByDomainEvent($domain, $event) { + return WebHook::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(WebHook::Schema()->Columns()->domain, $domain), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(WebHook::Schema()->Columns()->event, $event) + ) + ); + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/dto/AccessLog.php b/packages/backend-php/app/dto/AccessLog.php new file mode 100644 index 000000000..ca979c829 --- /dev/null +++ b/packages/backend-php/app/dto/AccessLog.php @@ -0,0 +1,33 @@ +id = $accessLog->id; + $this->key = $accessLog->key; + $this->type = $accessLog->type; + $this->authorized = $accessLog->authorized; + $this->from_ip = $accessLog->from_ip; + $this->time = $accessLog->time; + $this->userid = $accessLog->userid; + } +} diff --git a/packages/backend-php/app/dto/AccessToken.php b/packages/backend-php/app/dto/AccessToken.php new file mode 100644 index 000000000..64399b2c2 --- /dev/null +++ b/packages/backend-php/app/dto/AccessToken.php @@ -0,0 +1,27 @@ +id = $accessToken->id; + $this->token = $accessToken->token; + $this->expires = $accessToken->expires; + $this->userid = $accessToken->userid; + $this->appclientid = $accessToken->appclientid; + } +} diff --git a/packages/backend-php/app/dto/AppClient.php b/packages/backend-php/app/dto/AppClient.php new file mode 100644 index 000000000..5908977f2 --- /dev/null +++ b/packages/backend-php/app/dto/AppClient.php @@ -0,0 +1,39 @@ +id = $appClient->id; + $this->secret = $appClient->secret; + $this->expires = $appClient->expires; + $this->userid = $appClient->userid; + $this->name = $appClient->name; + $this->description = $appClient->description; + $this->url = $appClient->url; + $this->redirecturi = $appClient->redirecturi; + $this->enabled = $appClient->enabled; + } +} diff --git a/packages/backend-php/app/dto/AppClientInfo.php b/packages/backend-php/app/dto/AppClientInfo.php new file mode 100644 index 000000000..c15230330 --- /dev/null +++ b/packages/backend-php/app/dto/AppClientInfo.php @@ -0,0 +1,25 @@ +name = $clientInfo->name; + $this->description = $clientInfo->description; + $this->redirecturi = $clientInfo->redirecturi; + $this->url = $clientInfo->url; + } +} diff --git a/packages/backend-php/app/dto/EmailTemplate.php b/packages/backend-php/app/dto/EmailTemplate.php new file mode 100644 index 000000000..21614fba3 --- /dev/null +++ b/packages/backend-php/app/dto/EmailTemplate.php @@ -0,0 +1,33 @@ +id = $emailTemplate->id; + $this->name = $emailTemplate->name; + $this->code = $emailTemplate->code; + $this->subject = $emailTemplate->subject; + $this->help = $emailTemplate->help; + $this->body = $emailTemplate->body; + $this->html = $emailTemplate->html; + } +} diff --git a/packages/backend-php/app/dto/Event.php b/packages/backend-php/app/dto/Event.php new file mode 100644 index 000000000..feed23a23 --- /dev/null +++ b/packages/backend-php/app/dto/Event.php @@ -0,0 +1,30 @@ +id = $event->id; + $this->name = $event->name; + $this->domain = $event->domain; + $this->event = $event->event; + $this->description = $event->description; + $this->enabled = $event->enabled; + } +} diff --git a/packages/backend-php/app/dto/GeneratedEmailResults.php b/packages/backend-php/app/dto/GeneratedEmailResults.php new file mode 100644 index 000000000..7d039501f --- /dev/null +++ b/packages/backend-php/app/dto/GeneratedEmailResults.php @@ -0,0 +1,14 @@ +id = $genuineCard->id; + $this->key = $genuineCard->key; + $this->created = $genuineCard->created; + $this->issued = $genuineCard->issued; + $this->active = $genuineCard->active; + $this->paymentid = $genuineCard->paymentid; + $this->userid = $genuineCard->userid; + $this->owneremail = $genuineCard->owneremail; + $this->notes = $genuineCard->notes; + } +} diff --git a/packages/backend-php/app/dto/Ipn.php b/packages/backend-php/app/dto/Ipn.php new file mode 100644 index 000000000..1fd1865ce --- /dev/null +++ b/packages/backend-php/app/dto/Ipn.php @@ -0,0 +1,44 @@ +id = $ipn->id; + $this->created = $ipn->created; + $this->validation = EnumMapper::tryFrom(IpnValidationEnum::cases(), $ipn->validation); + $this->payment_status = $ipn->payment_status; + $this->payment_amount = $ipn->payment_amount; + $this->payment_currency = $ipn->payment_currency; + $this->payer_email = $ipn->payer_email; + $this->item_name = $ipn->item_name; + $this->item_number = $ipn->item_number; + $this->raw = $ipn->raw; + } +} diff --git a/packages/backend-php/app/dto/IpnValidationEnum.php b/packages/backend-php/app/dto/IpnValidationEnum.php new file mode 100644 index 000000000..5139c284d --- /dev/null +++ b/packages/backend-php/app/dto/IpnValidationEnum.php @@ -0,0 +1,13 @@ +id = $key->id; + $this->userid = $key->userid; + $this->type = EnumMapper::tryFrom(KeyTypeEnum::cases(), $key->type); + $this->key = $key->key; + $this->created = $key->created; + $this->notes = $key->notes; + $this->expires = $key->expires; + } +} diff --git a/packages/backend-php/app/dto/KeyTypeEnum.php b/packages/backend-php/app/dto/KeyTypeEnum.php new file mode 100644 index 000000000..768bee088 --- /dev/null +++ b/packages/backend-php/app/dto/KeyTypeEnum.php @@ -0,0 +1,18 @@ +id = $membership->id; + $this->title = $membership->title; + $this->code = $membership->code; + $this->description = $membership->description; + $this->price = $membership->price; + $this->days = $membership->days; + $this->period = $membership->period; + $this->trial = $membership->trial; + $this->recurring = $membership->recurring; + $this->private = $membership->private; + $this->active = $membership->active; + } +} diff --git a/packages/backend-php/app/dto/PasswordResetRequest.php b/packages/backend-php/app/dto/PasswordResetRequest.php new file mode 100644 index 000000000..abaeb9e6a --- /dev/null +++ b/packages/backend-php/app/dto/PasswordResetRequest.php @@ -0,0 +1,24 @@ +id = $passwordResetRequest->id; + $this->userid = $passwordResetRequest->userid; + $this->token = $passwordResetRequest->token; + $this->created = $passwordResetRequest->created; + } +} diff --git a/packages/backend-php/app/dto/Payment.php b/packages/backend-php/app/dto/Payment.php new file mode 100644 index 000000000..722b87f2f --- /dev/null +++ b/packages/backend-php/app/dto/Payment.php @@ -0,0 +1,59 @@ +id = $payment->id; + $this->txn_id = $payment->txn_id; + $this->membership_id = $payment->membership_id; + $this->user_id = $payment->user_id; + $this->payer_email = $payment->payer_email; + $this->payer_fname = $payment->payer_fname; + $this->payer_lname = $payment->payer_lname; + $this->rate_amount = $payment->rate_amount; + $this->currency = $payment->currency; + $this->date = $payment->date; + $this->pp = EnumMapper::tryFrom(PaymentPpEnum::cases(), $payment->pp); + $this->ip = $payment->ip; + $this->status = $payment->status; + $this->item_name = $payment->item_name; + $this->item_number = $payment->item_number; + } +} diff --git a/packages/backend-php/app/dto/PaymentPpEnum.php b/packages/backend-php/app/dto/PaymentPpEnum.php new file mode 100644 index 000000000..e27ebea73 --- /dev/null +++ b/packages/backend-php/app/dto/PaymentPpEnum.php @@ -0,0 +1,14 @@ +id = $privilege->id; + $this->name = $privilege->name; + $this->code = $privilege->code; + $this->description = $privilege->description; + $this->icon = $privilege->icon; + $this->enabled = $privilege->enabled; + } +} diff --git a/packages/backend-php/app/dto/RefreshToken.php b/packages/backend-php/app/dto/RefreshToken.php new file mode 100644 index 000000000..3a4ebbb8c --- /dev/null +++ b/packages/backend-php/app/dto/RefreshToken.php @@ -0,0 +1,27 @@ +id = $refreshToken->id; + $this->token = $refreshToken->token; + $this->expires = $refreshToken->expires; + $this->userid = $refreshToken->userid; + $this->appclientid = $refreshToken->appclientid; + } +} diff --git a/packages/backend-php/app/dto/SavedRefreshToken.php b/packages/backend-php/app/dto/SavedRefreshToken.php new file mode 100644 index 000000000..b96cfc415 --- /dev/null +++ b/packages/backend-php/app/dto/SavedRefreshToken.php @@ -0,0 +1,17 @@ +success = $success; + $this->msg = $msg; + } +} diff --git a/packages/backend-php/app/dto/ServiceResponseError.php b/packages/backend-php/app/dto/ServiceResponseError.php new file mode 100644 index 000000000..7f7b95df1 --- /dev/null +++ b/packages/backend-php/app/dto/ServiceResponseError.php @@ -0,0 +1,8 @@ +id = $stripeEvent->id; + $this->ts = $stripeEvent->ts; + $this->status = EnumMapper::tryFrom(StripeEventStatusEnum::cases(), $stripeEvent->status); + $this->created = $stripeEvent->created; + $this->event_id = $stripeEvent->event_id; + $this->type = $stripeEvent->type; + $this->object = $stripeEvent->object; + $this->request = $stripeEvent->request; + $this->api_version = $stripeEvent->api_version; + $this->raw = $stripeEvent->raw; + } +} diff --git a/packages/backend-php/app/dto/StripeEventStatusEnum.php b/packages/backend-php/app/dto/StripeEventStatusEnum.php new file mode 100644 index 000000000..96f34b5d1 --- /dev/null +++ b/packages/backend-php/app/dto/StripeEventStatusEnum.php @@ -0,0 +1,13 @@ +id = $systemPreference->id; + $this->key = $systemPreference->key; + $this->value = $systemPreference->value; + $this->enabled = $systemPreference->enabled; + $this->notes = $systemPreference->notes; + } +} diff --git a/packages/backend-php/app/dto/TotalKeyHoldersResult.php b/packages/backend-php/app/dto/TotalKeyHoldersResult.php new file mode 100644 index 000000000..64dc3958e --- /dev/null +++ b/packages/backend-php/app/dto/TotalKeyHoldersResult.php @@ -0,0 +1,7 @@ +id = $clientInfo->id; + $this->name = $clientInfo->name; + $this->description = $clientInfo->description; + $this->enabled = $clientInfo->enabled; + $this->expires = $clientInfo->expires; + $this->owner = new TrimmedUser($clientInfo->owner); + } +} diff --git a/packages/backend-php/app/dto/TrimmedAppClientOwner.php b/packages/backend-php/app/dto/TrimmedAppClientOwner.php new file mode 100644 index 000000000..cc56278f3 --- /dev/null +++ b/packages/backend-php/app/dto/TrimmedAppClientOwner.php @@ -0,0 +1,35 @@ +id = $userInfo->id; + $this->username = $userInfo->username; + $this->email = $userInfo->email; + $this->fname = $userInfo->fname; + $this->lname = $userInfo->lname; + $this->membership = $userInfo->membership; + $this->created = $userInfo->created; + $this->active = $userInfo->active; + $this->privileges = $userInfo->privileges; + } +} diff --git a/packages/backend-php/app/dto/TrimmedUser.php b/packages/backend-php/app/dto/TrimmedUser.php new file mode 100644 index 000000000..a0b5535bd --- /dev/null +++ b/packages/backend-php/app/dto/TrimmedUser.php @@ -0,0 +1,35 @@ +id = $userInfo->id; + $this->username = $userInfo->username; + $this->email = $userInfo->email; + $this->fname = $userInfo->fname; + $this->lname = $userInfo->lname; + $this->membership = $userInfo->membership; + $this->created = $userInfo->created; + $this->active = $userInfo->active; + $this->privileges = $userInfo->privileges; + } +} diff --git a/packages/backend-php/app/dto/User.php b/packages/backend-php/app/dto/User.php new file mode 100644 index 000000000..1acaa140c --- /dev/null +++ b/packages/backend-php/app/dto/User.php @@ -0,0 +1,84 @@ +id = $user->id; + $this->username = $user->username; + $this->password = $user->password; + $this->membership_id = $user->membership_id; + $this->mem_expire = $user->mem_expire; + $this->trial_used = $user->trial_used; + $this->email = $user->email; + $this->fname = $user->fname; + $this->lname = $user->lname; + $this->token = $user->token; + $this->cookie_id = $user->cookie_id; + $this->newsletter = $user->newsletter; + $this->cash = $user->cash; + $this->userlevel = $user->userlevel; + $this->notes = $user->notes; + $this->created = $user->created; + $this->lastlogin = $user->lastlogin; + $this->lastip = $user->lastip; + $this->avatar = $user->avatar; + $this->active = $user->active; + $this->paypal_id = $user->paypal_id; + $this->payment_email = $user->payment_email; + $this->stripe_id = $user->stripe_id; + $this->stripe_email = $user->stripe_email; + } +} diff --git a/packages/backend-php/app/dto/UserActiveEnum.php b/packages/backend-php/app/dto/UserActiveEnum.php new file mode 100644 index 000000000..661857444 --- /dev/null +++ b/packages/backend-php/app/dto/UserActiveEnum.php @@ -0,0 +1,15 @@ +id = $userPrincipal['id']; + $this->permissions = $userPrincipal['permissions']; + } +} diff --git a/packages/backend-php/app/dto/WebHook.php b/packages/backend-php/app/dto/WebHook.php new file mode 100644 index 000000000..80e116353 --- /dev/null +++ b/packages/backend-php/app/dto/WebHook.php @@ -0,0 +1,42 @@ +id = $webHook->id; + $this->name = $webHook->name; + $this->description = $webHook->description; + $this->enabled = $webHook->enabled; + $this->userid = $webHook->userid; + $this->url = $webHook->url; + $this->translation = $webHook->translation; + $this->headers = $webHook->headers; + $this->method = $webHook->method; + $this->eventid = $webHook->eventid; + } +} diff --git a/packages/backend-php/app/dto/v2/MetricServiceGetCreatedDatesResult.php b/packages/backend-php/app/dto/v2/MetricServiceGetCreatedDatesResult.php new file mode 100644 index 000000000..554f4ed36 --- /dev/null +++ b/packages/backend-php/app/dto/v2/MetricServiceGetCreatedDatesResult.php @@ -0,0 +1,18 @@ +> $byDowHour + * @property array<"1"|"10"|"11"|"12"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9",array<"0"|"1"|"2"|"3"|"4"|"5"|"6"|"total",int>> $byMonthDow + * + * @typescript + */ +class MetricServiceGetCreatedDatesResult extends DTO { +} diff --git a/packages/backend-php/app/dto/v2/MetricServiceGetMembersResult.php b/packages/backend-php/app/dto/v2/MetricServiceGetMembersResult.php new file mode 100644 index 000000000..d50463621 --- /dev/null +++ b/packages/backend-php/app/dto/v2/MetricServiceGetMembersResult.php @@ -0,0 +1,53 @@ + $created + * @property array $expired + * @property array $total + * + * @typescript + */ +class MetricServiceGetMembersResult extends DTO implements IDTO { + /** + * jsonSerialize. + * + * @return mixed + */ + public function jsonSerialize(): mixed { + return $this->__serialize(); + } + + /** + * __serialize. + * + * @return array{start_range: string, end_range: string, created: non-empty-array|stdClass, expired: non-empty-array|stdClass, total: non-empty-array|stdClass} + */ + public function __serialize(): array { + return [ + 'start_range' => $this->start_range, + 'end_range' => $this->end_range, + 'created' => !empty($this->created) ? $this->created : new stdClass(), + 'expired' => count(value: $this->expired) !== 0 ? $this->expired : new stdClass(), + 'total' => !empty($this->total) ? $this->total : new stdClass() + ]; + } + + /** + * __toString. + * + * @return string + */ + public function __toString(): string { + return json_encode($this->jsonSerialize()); + } +} diff --git a/packages/backend-php/app/dto/v2/MetricServiceGetRevenueResult.php b/packages/backend-php/app/dto/v2/MetricServiceGetRevenueResult.php new file mode 100644 index 000000000..c13b939af --- /dev/null +++ b/packages/backend-php/app/dto/v2/MetricServiceGetRevenueResult.php @@ -0,0 +1,18 @@ +|int>|string $grouping + * @property array<\app\enums\MetricServiceGroupType,array> $by_membership + * + * @typescript + */ +class MetricServiceGetRevenueResult extends DTO { +} diff --git a/packages/backend-php/app/dto/v2/MetricServiceNewKeyholdersResult.php b/packages/backend-php/app/dto/v2/MetricServiceNewKeyholdersResult.php new file mode 100644 index 000000000..ee442138e --- /dev/null +++ b/packages/backend-php/app/dto/v2/MetricServiceNewKeyholdersResult.php @@ -0,0 +1,15 @@ +validation; } + /** + * Name. + * + * @return string + */ public function Name() { return 'paypal'; } + /** + * Process. + * + * @param mixed $data + * + * @throws \app\gateways\PaymentGatewayException + * + * @return string + */ public function Process($data) { // Put this url into paypal // IPN URL: http://cook.vanhack.ca:8888/services/gateways/paypal @@ -59,6 +94,7 @@ public function Process($data) { // Step 2: POST IPN data back to PayPal to validate // ToDo: this should be an option available from the admin interface $paypal = 'https://ipnpb.paypal.com/cgi-bin/webscr'; + // @phpstan-ignore if.alwaysFalse if (DEBUG) { $paypal = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr'; } @@ -66,12 +102,12 @@ public function Process($data) { $ch = curl_init($paypal); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $req); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($ch, CURLOPT_FORBID_REUSE, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($ch, CURLOPT_FORBID_REUSE, true); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Connection: Close']); $error = null; @@ -103,10 +139,12 @@ public function Process($data) { } elseif (strcmp($response, 'INVALID') == 0) { // IPN invalid, log for manual investigation $result = $this->CreateInvalidIPNRecord($req); + throw new PaymentGatewayException('Error: Could not validate paypal IPN ' . $req); } $this->CreateInvalidIPNRecord($req); + throw new PaymentGatewayException('Error: Unknown Paypal IPN Error ' . $req); } } diff --git a/app/gateways/StripeGateway.php b/packages/backend-php/app/gateways/StripeGateway.php similarity index 66% rename from app/gateways/StripeGateway.php rename to packages/backend-php/app/gateways/StripeGateway.php index 5eacaec10..c34bfa253 100644 --- a/app/gateways/StripeGateway.php +++ b/packages/backend-php/app/gateways/StripeGateway.php @@ -9,14 +9,31 @@ use app\domain\StripeEvent; use Stripe\Stripe; +use vhs\web\enums\HttpStatusCodes; +/** @typescript */ class StripeGateway implements IPaymentGateway { public function __construct() { - \Stripe\Stripe::setApiKey(STRIPE_API_KEY); + Stripe::setApiKey(STRIPE_API_KEY); } + /** + * CreateStripeEventRecord. + * + * @param mixed $status + * @param mixed $created + * @param mixed $event_id + * @param mixed $type + * @param mixed $object + * @param mixed $request + * @param mixed $api_version + * @param mixed $raw + * + * @return StripeEvent + */ public function CreateStripeEventRecord($status, $created, $event_id, $type, $object, $request, $api_version, $raw) { // Create the IPN record in the database + /** @var \app\domain\StripeEvent $stripe_event */ $stripe_event = new StripeEvent(); $stripe_event->status = $status; @@ -31,15 +48,30 @@ public function CreateStripeEventRecord($status, $created, $event_id, $type, $ob return $stripe_event; } + /** + * Name. + * + * @return string + */ public function Name() { return 'stripe'; } + /** + * Process. + * + * @param mixed $payload + * + * @throws \app\gateways\PaymentGatewayException + * + * @return mixed + */ public function Process($payload) { if (!isset($_SERVER['HTTP_STRIPE_SIGNATURE'])) { + http_response_code(HttpStatusCodes::Client_Error_Bad_Request->value); + throw new PaymentGatewayException('Error: Unknown Stripe Event Error: Missing signature'); - http_response_code(400); - exit(); + // return; } $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE']; @@ -47,17 +79,7 @@ public function Process($payload) { try { $event = \Stripe\Webhook::constructEvent($payload, $sig_header, STRIPE_WEBHOOK_SECRET); - } catch (\UnexpectedValueException $e) { - // Invalid payload - throw new PaymentGatewayException('Error: Unknown Stripe Event Error ' . $payload); - http_response_code(400); - exit(); - } catch (\Stripe\Exception\SignatureVerificationException $e) { - // Invalid signature - throw new PaymentGatewayException('Error: Unknown Stripe Event Error ' . $payload); - http_response_code(400); - exit(); - } finally { + // Set up the initial event $event_record = $this->CreateStripeEventRecord( 'UNKNOWN', @@ -73,18 +95,31 @@ public function Process($payload) { // Handle the event switch ($event->type) { case 'invoice.paid': + // @phpstan-ignore property.notFound $paymentIntent = $event->data->object; // contains a StripePaymentIntent $event_record->status = 'VALID'; $event_record->save(); + break; default: $event_record->save(); + throw new PaymentGatewayException('Received unknown event type ' . $event->type); } http_response_code(200); return json_encode(['result' => 'OK']); + } catch (\UnexpectedValueException $e) { + // Invalid payload + http_response_code(HttpStatusCodes::Client_Error_Bad_Request->value); + + return new PaymentGatewayException('Error: Unknown Stripe Event Error ' . $payload); + } catch (\Stripe\Exception\SignatureVerificationException $e) { + // Invalid signature + http_response_code(HttpStatusCodes::Client_Error_Bad_Request->value); + + return new PaymentGatewayException('Error: Unknown Stripe Event Error ' . $payload); } } } diff --git a/packages/backend-php/app/handlers/v2/ApiKeyServiceHandler2.php b/packages/backend-php/app/handlers/v2/ApiKeyServiceHandler2.php new file mode 100644 index 000000000..c867cbcd4 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/ApiKeyServiceHandler2.php @@ -0,0 +1,219 @@ +userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + $key->delete(); + } + + /** + * @permission administrator + * + * @param string $notes + * + * @return \app\domain\Key + */ + public function GenerateSystemApiKey($notes): Key { + $apiKey = new Key(); + + $apiKey->key = bin2hex(openssl_random_pseudo_bytes(32)); + $apiKey->type = 'api'; + $apiKey->notes = $notes; + + $apiKey->save(); + + return $apiKey; + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param string $notes + * + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key + */ + public function GenerateUserApiKey($userid, $notes): Key { + if (!CurrentUser::hasAnyPermissions('administrator') && $userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + $user = User::find($userid); + + if (is_null($user)) { + throw new InvalidInputException('Invalid userid'); + } + + $apiKey = new Key(); + + $apiKey->key = bin2hex(openssl_random_pseudo_bytes(32)); + $apiKey->type = 'api'; + $apiKey->notes = $notes; + + $user->keys->add($apiKey); + + $user->save(); + + return $apiKey; + } + + /** + * @permission administrator|user + * + * @param int $keyid + * + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key + */ + public function GetApiKey($keyid): Key { + /** @var \app\domain\Key|null $key */ + $key = Key::find($keyid); + + if (is_null($key)) { + throw new InvalidInputException(\app\constants\Errors::E_INVALID_KEY_INPUT); + } + + if (!CurrentUser::hasAnyPermissions('administrator')) { + if (is_null($key->userid) || $key->userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + } + + return $key; + } + + /** + * @permission administrator + * + * @return \app\domain\Key[] + */ + public function GetSystemApiKeys(): array { + return Key::getSystemApiKeys(); + } + + /** + * @permission administrator|user + * + * @param int $userid + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key[] + */ + public function GetUserApiKeys($userid): array { + if (!CurrentUser::hasAnyPermissions('administrator') && $userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + return Key::getUserApiKeys($userid); + } + + /** + * @permission administrator|user + * + * @param int $keyid + * @param string|string[] $privileges + * + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return bool + */ + public function PutApiKeyPrivileges($keyid, $privileges): bool { + /** @var \app\domain\Key|null */ + $key = Key::find($keyid); + + if (is_null($key)) { + throw new InvalidInputException(\app\constants\Errors::E_INVALID_KEY_INPUT); + } + + if (!CurrentUser::hasAnyPermissions('administrator')) { + if (is_null($key->userid) || $key->userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + } + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + foreach ($key->privileges->all() as $priv) { + $key->privileges->remove($priv); + } + + foreach ($privs as $priv) { + $key->privileges->add($priv); + } + + return $key->save(); + } + + /** + * @permission administrator|user + * + * @param int $keyid + * @param string $notes + * @param string|null $expires + * + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return bool + */ + public function UpdateApiKey($keyid, $notes, $expires): bool { + /** @var \app\domain\Key */ + $key = Key::find($keyid); + + if (is_null($key)) { + throw new InvalidInputException(\app\constants\Errors::E_INVALID_KEY_INPUT); + } + + if (!CurrentUser::hasAnyPermissions('administrator') && (is_null($key->userid) || $key->userid != CurrentUser::getIdentity())) { + throw new UnauthorizedException(); + } + + $key->notes = $notes; + $key->expires = $expires; + + return $key->save(); + } +} diff --git a/packages/backend-php/app/handlers/v2/AuthServiceHandler2.php b/packages/backend-php/app/handlers/v2/AuthServiceHandler2.php new file mode 100644 index 000000000..8394a3023 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/AuthServiceHandler2.php @@ -0,0 +1,471 @@ +valid) { + $retval->valid = true; + $retval->userId = $user->id; + $retval->username = $user->username; + $retval->type = $user->membership->code; + $retval->privileges = $key->getAbsolutePrivileges(); + + return true; + } elseif (!is_null($user) && $user instanceof User) { + $retval->username = $user->username; + $retval->message = $user->getInvalidReason(); + } else { + $retval->username = 'unknown'; + $retval->message = 'Null user'; + } + + return false; + } + + /** + * Check to see if the user pin and account is valid. + * + * @permission administrator|pin-auth + * + * @param string $pin + * + * @return \app\utils\AuthCheckResult + */ + public function CheckPin($pin): AuthCheckResult { + // pin magic + // TODO: documentation + $pin = str_replace('|', '', $pin); + + $intpin = intval($pin); + + $pinid = intval($intpin / 10000); + + $pin = $intpin - $pinid * 10000; + + $pinid = sprintf('%04s', $pinid); + $pin = sprintf('%04s', $pin); + + // Set defaults + $retval = new AuthCheckResult(); + + // Find key by pin + /** @var \app\domain\Key[] */ + $keys = Key::findByPin($pinid . '|' . $pin); + + $logAccess = function ($granted, $userid = null) use ($pinid, $pin) { + try { + AccessLog::log($pinid . '|' . $pin, 'pin', $granted, $_SERVER['REMOTE_ADDR'] ?? 'UNKNOWN', $userid); + } catch (\Exception $ex) { + /*mmm*/ + } + }; + + // If we get an invalid result, log and fail (we should always only get one result) + if (count($keys) != 1) { + $logAccess(false); + + return $retval; + } + + // Get key + $key = $keys[0]; + + // If missing userid, log and fail + if ($key->userid == null) { + $logAccess(false); + + return $retval; + } + + // Fetch userinfo + $user = User::find($key->userid); + + // Check if we have a user from the key + if ($user == null || !$user instanceof User) { + $logAccess(false); + + return $retval; + } + + // Check if account is active and in good standing, and return result set + $isValid = self::parseValidAccount($key, $user, $retval); + + // Log + $logAccess($isValid, $user->id); + + // Return + return $retval; + } + + /** + * @permission administrator|rfid-auth + * + * @param string $rfid + * + * @return \app\utils\AuthCheckResult + */ + public function CheckRfid($rfid): AuthCheckResult { + // Set defaults + $retval = new AuthCheckResult(); + + // Find key by RFID card id + $keys = Key::findByRfid($rfid); + + $logAccess = function ($granted, $userid = null) use ($rfid) { + try { + AccessLog::log('rfid', $rfid, $granted, $_SERVER['REMOTE_ADDR'], $userid); + } catch (\Exception $ex) { + /*mmm*/ + } + }; + + // If we get an invalid result, log and fail (we should always only get one result) + if (count($keys) != 1) { + $logAccess(false); + + return $retval; + } + + // Fetch key + $key = $keys[0]; + + // Check if there's a userid attached to the key, else fail + if ($key->userid == null) { + $logAccess(false); + + return $retval; + } + + // Fetch userinfo + $user = User::find($key->userid); + + // Check if we have a user from the key + if ($user == null || !$user instanceof User) { + $logAccess(false); + + return $retval; + } + + // Check if account is active and in good standing, and return result set + $isValid = self::parseValidAccount($key, $user, $retval); + + // Log + $logAccess($isValid, $user->id); + + // Return + return $retval; + } + + /** + * Check to see if the user service/id is valid. A service could be github/slack/google. + * + * @permission administrator|service-auth + * + * @param string $service + * @param string $id + * + * @return \app\utils\AuthCheckResult + */ + public function CheckService($service, $id): AuthCheckResult { + // Set defaults + $retval = new AuthCheckResult(); + + // Always parse service names as lowercase + $service = strtolower($service); + + // Find service key + $keys = Key::findByService($service, $id); + + $logAccess = function ($granted, $userid = null) use ($service, $id) { + try { + AccessLog::log($id, $service, $granted, $_SERVER['REMOTE_ADDR'], $userid); + } catch (\Exception $ex) { + /*mmm*/ + } + }; + + // If we get an invalid result, log and fail (we should always only get one result) + if (count($keys) != 1) { + $logAccess(false); + + return $retval; + } + + // Fetch key + $key = $keys[0]; + + // Check if there's a userid attached to the key, else fail + if ($key->userid == null) { + $logAccess(false); + + return $retval; + } + + // Fetch userinfo + $user = User::find($key->userid); + + // Check if we have a user from the key + if ($user == null || !$user instanceof User) { + $logAccess(false); + + return $retval; + } + + // Check if account is active and in good standing, and return result set + $isValid = self::parseValidAccount($key, $user, $retval); + + // Log + $logAccess($isValid, $user->id); + + // Return + return $retval; + } + + /** + * @permission anonymous + * + * @param string $username + * + * @return bool + */ + public function CheckUsername($username): bool { + return Database::exists( + new QuerySelect(UserSchema::Table(), UserSchema::Column('username'), Where::Equal(UserSchema::Column('username'), $username)) + ); + } + + /** + * @permission administrator + * + * @param string|\vhs\domain\Filter|null $filters + * + * @return int + */ + public function CountAccessLog($filters): int { + return AccessLog::count($filters); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param \vhs\domain\Filter|null $filters + * + * @return int + */ + public function CountUserAccessLog($userid, $filters): int { + $filters = $this->addUserIDToFilters($userid, $filters); + + return AccessLog::count($filters); + } + + /** + * @permission anonymous + * + * @return \app\security\UserPrincipal|\vhs\security\AnonPrincipal|\vhs\security\IPrincipal + */ + public function CurrentUser(): IPrincipal|UserPrincipal|AnonPrincipal { + return CurrentUser::getPrincipal(); + } + + /** + * @permission oauth-provider + * + * @param string $username + * @param string $password + * + * @return \app\dto\TrimmedUser|null + */ + public function GetUser($username, $password): TrimmedUser|null { + return $this->trimUser(Authenticate::authenticateOnly($username, $password)); + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters + * + * @return \app\domain\AccessLog[] + */ + public function ListAccessLog($page, $size, $columns, $order, $filters): array { + return AccessLog::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\AccessLog[] + */ + public function ListUserAccessLog($userid, $page, $size, $columns, $order, $filters): array { + $filters = $this->addUserIDToFilters($userid, $filters); + + return AccessLog::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission anonymous + * + * @param string $username + * @param string $password + * + * @return string + */ + public function Login($username, $password): string { + try { + Authenticate::getInstance()->login(new UserPassCredentials($username, $password)); + } catch (\Exception $ex) { + return $ex->getMessage(); + } + + return StringLiterals::AUTH_ACCESS_GRANTED; + } + + /** + * @permission user + * + * @return void + */ + public function Logout(): void { + Authenticate::getInstance()->logout(); + } + + /** + * @permission anonymous + * + * @param string $pin + * + * @throws \vhs\exceptions\HttpException + * + * @return string + */ + public function PinLogin($pin): string { + try { + Authenticate::getInstance()->login(new PinCredentials($pin)); + } catch (\Exception $ex) { + throw new HttpException(StringLiterals::AUTH_ACCESS_DENIED, HttpStatusCodes::Client_Error_Forbidden); + } + + return StringLiterals::AUTH_ACCESS_GRANTED; + } + + /** + * @permission anonymous + * + * @param string $key + * + * @throws \vhs\exceptions\HttpException + * + * @return string + */ + public function RfidLogin($key): string { + try { + Authenticate::getInstance()->login(new PinCredentials($key)); + } catch (\Exception $ex) { + throw new HttpException(StringLiterals::AUTH_ACCESS_DENIED, HttpStatusCodes::Client_Error_Forbidden); + } + + return StringLiterals::AUTH_ACCESS_GRANTED; + } + + /** + * Summary of AddUserIDToFilters. + * + * @param mixed $userid + * @param string|\vhs\domain\Filter|null $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \vhs\domain\Filter + */ + private function addUserIDToFilters($userid, $filters): Filter { + $userService2 = new UserServiceHandler2(); + $user = $userService2->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::Equal('userid', $user->id); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + return $filters; + } + + /** + * Summary of trimUser. + * + * @param \app\domain\User|null $user + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\dto\TrimmedUser + */ + private function trimUser($user): TrimmedUser { + if (is_null($user)) { + throw new HttpException('Client not found', HttpStatusCodes::Client_Error_Not_Found); + } + + return new TrimmedUser($user); + } +} diff --git a/packages/backend-php/app/handlers/v2/EmailServiceHandler2.php b/packages/backend-php/app/handlers/v2/EmailServiceHandler2.php new file mode 100644 index 000000000..5e6db0370 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/EmailServiceHandler2.php @@ -0,0 +1,266 @@ +delete(); + } + + /** + * Summary of Email. + * + * @permission administrator + * + * @param string $email + * @param string $tmpl + * @param array $context + * @param string|null $subject + * + * @return void + */ + public function Email($email, $tmpl, $context, $subject = null): void { + EmailAdapter2::getInstance()->Email($email, $tmpl, $context, $subject); + } + + /** + * Summary of EmailUser. + * + * @permission administrator + * + * @param \app\domain\User $user email address + * @param string $tmpl + * @param array $context + * @param string|null $subject + * + * @return void + */ + public function EmailUser($user, $tmpl, $context, $subject = null): void { + EmailAdapter2::getInstance()->EmailUser($user, $tmpl, $context, $subject); + } + + /** + * @permission administrator + * + * @param int $id + * + * @return \app\domain\EmailTemplate|null + */ + public function GetTemplate($id): EmailTemplate|null { + /** @var \app\domain\EmailTemplate|null */ + return EmailTemplate::find($id); + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\EmailTemplate[] + */ + public function ListTemplates($page, $size, $columns, $order, $filters): array { + /** @var \app\domain\EmailTemplate[] */ + return EmailTemplate::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param string $name + * @param string $code + * @param string $subject + * @param string $help + * @param string $body + * @param string $html + * + * @return bool + */ + public function PutTemplate($name, $code, $subject, $help, $body, $html): bool { + /** @var EmailTemplate|null */ + $template = EmailTemplate::findByCode($code); + + if (is_null($template)) { + $template = new EmailTemplate(); + } + + $template->name = $name; + $template->code = $code; + $template->subject = $subject; + $template->help = $help; + $template->body = $body; + $template->html = $html; + + return $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $name + * @param string $code + * @param string $subject + * @param string $help + * @param string $body + * @param string $html + * + * @return bool + */ + public function UpdateTemplate($id, $name, $code, $subject, $help, $body, $html): bool { + /** @var EmailTemplate|null */ + $template = EmailTemplate::find($id); + + if (is_null($template)) { + throw new HttpException('Invalid or missing template id.', HttpStatusCodes::Client_Error_Not_Found); + } + + $template->name = $name; + $template->code = $code; + $template->subject = $subject; + $template->help = $help; + $template->body = $body; + $template->html = $html; + + return $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $body + * + * @return bool + */ + public function UpdateTemplateBody($id, $body): bool { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->body = $body; + + return $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $code + * + * @return bool + */ + public function UpdateTemplateCode($id, $code): bool { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->code = $code; + + return $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $help + * + * @return bool + */ + public function UpdateTemplateHelp($id, $help): bool { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->help = $help; + + return $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $html + * + * @return bool + */ + public function UpdateTemplateHtml($id, $html): bool { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->html = $html; + + return $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $name + * + * @return bool + */ + public function UpdateTemplateName($id, $name): bool { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->name = $name; + + return $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $subject + * + * @return bool + */ + public function UpdateTemplateSubject($id, $subject): bool { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->subject = $subject; + + return $template->save(); + } +} diff --git a/packages/backend-php/app/handlers/v2/EventServiceHandler2.php b/packages/backend-php/app/handlers/v2/EventServiceHandler2.php new file mode 100644 index 000000000..26167745d --- /dev/null +++ b/packages/backend-php/app/handlers/v2/EventServiceHandler2.php @@ -0,0 +1,276 @@ +name = $name; + $evt->domain = $domain; + $evt->event = $event; + $evt->description = $description; + $evt->enabled = $enabled; + + $evt->save(); + + return $evt; + } + + /** + * @permission administrator + * + * @param int $id + * + * @return void + */ + public function DeleteEvent($id): void { + $event = $this->getEventById($id); + + $event->delete(); + } + + /** + * @permission administrator + * + * @param int $id + * @param bool $enabled + * + * @return bool + */ + public function EnableEvent($id, $enabled): bool { + $evt = $this->getEventById($id); + + $evt->enabled = $enabled; + + return $evt->save(); + } + + /** + * @permission user + * + * @return \app\domain\Event[] + */ + public function GetAccessibleEvents(): array { + $events = Event::findAll(); + + if (CurrentUser::hasAllPermissions('administrator')) { + return $events; + } + + $retval = []; + + foreach ($events as $event) { + $privs = []; + foreach ($event->privileges->all() as $priv) { + array_push($privs, $priv->code); + } + + if (CurrentUser::hasAllPermissions(...$privs)) { + array_push($retval, $event); + } + } + + return $retval; + } + + /** + * @permission webhook|administrator + * + * @param string $domain + * + * @return void + */ + public function GetDomainDefinition($domain): void { + // TODO: Implement GetDomainDefinition() method. + } + + /** + * @permission webhook|administrator + * + * @return mixed + */ + public function GetDomainDefinitions(): mixed { + foreach (glob('domain/*.php') as $filename) { + include_once $filename; + } + + $domains = []; + + foreach (get_declared_classes() as $class) { + if (is_subclass_of($class, '\\vhs\\domain\\Domain')) { + $name = str_replace('app\\domain\\', '', $class); + $domains[$name] = [ + 'checks' => $class::Schema()->Table()->checks + ]; + } + } + + return $domains; + } + + /** + * @permission administrator + * + * @param int $id + * + * @return \app\domain\Event + */ + public function GetEvent($id): Event { + return $this->getEventById($id); + } + + /** + * @permission webhook|administrator + * + * @return \app\domain\Event[] + */ + public function GetEvents(): array { + return Event::findAll(); + } + + /** + * @permission administrator + * + * @return string[] + */ + public function GetEventTypes(): array { + $updateKeys = array_filter(get_class_methods('vhs\domain\Domain'), function ($k) { + return preg_match('/^onAny/', $k); + }); + + sort($updateKeys); + + return array_map(fn ($method): string => str_replace('before', 'before:', strtolower(str_replace('onAny', '', $method))), $updateKeys); + } + + /** + * @permission webhook|administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\Event[] + */ + public function ListEvents($page, $size, $columns, $order, $filters): array { + /** @var \app\domain\Event[] */ + return Event::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param int $id + * @param string|string[] $privileges + * + * @return bool + */ + public function PutEventPrivileges($id, $privileges): bool { + $evt = $this->getEventById($id); + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + foreach ($evt->privileges->all() as $priv) { + $evt->privileges->remove($priv); + } + + foreach ($privs as $priv) { + $evt->privileges->add($priv); + } + + return $evt->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $name + * @param string $domain + * @param string $event + * @param string $description + * @param bool $enabled + * + * @return bool + */ + public function UpdateEvent($id, $name, $domain, $event, $description, $enabled): bool { + $evt = $this->getEventById($id); + + $evt->name = $name; + $evt->domain = $domain; + $evt->event = $event; + $evt->description = $description; + $evt->enabled = $enabled; + + return $evt->save(); + } + + /** + * Summary of getEventById. + * + * @param int $id + * + * @throws \app\exceptions\InvalidInputException + * + * @return \app\domain\Event + */ + private function getEventById($id): Event { + /** @var \app\domain\Event|null */ + $evt = Event::find($id); + + if (is_null($evt)) { + throw new InvalidInputException(Errors::E_INVALID_EVENT); + } + + return $evt; + } +} diff --git a/packages/backend-php/app/handlers/v2/IpnServiceHandler2.php b/packages/backend-php/app/handlers/v2/IpnServiceHandler2.php new file mode 100644 index 000000000..b5889f122 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/IpnServiceHandler2.php @@ -0,0 +1,67 @@ +getKeyById($id); + + $key->delete(); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param string $type + * @param string $value + * @param string $notes + * + * @throws \app\exceptions\InvalidInputException + * + * @return \app\domain\Key|null + */ + public function GenerateUserKey($userid, $type, $value, $notes): Key|null { + if (CurrentUser::getIdentity() == $userid || CurrentUser::hasAnyPermissions('administrator')) { + $user = User::find($userid); + + if ($user != null) { + $key = new Key(); + + switch ($type) { + case 'rfid': + $key->key = $value; + + break; + case 'pin': + $nextpinid = Database::scalar( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Query::Select(SettingsSchema::Table(), SettingsSchema::Columns()->nextpinid) + ); + $key->key = sprintf('%04s', $nextpinid) . '|' . sprintf('%04s', rand(0, 9999)); + /** @disregard P1006 override */ + $key->privileges->add(Privilege::findByCode('inherit')); + + break; + case 'api': + $key->key = bin2hex(openssl_random_pseudo_bytes(32)); + + break; + default: + throw new InvalidInputException('Unsupported key type'); + } + + $key->notes = $notes; + $key->type = $type; + $key->userid = $userid; + + $key->save(); + + return $key; + } + } + + return null; + } + + /** + * @permission administrator + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key[] + */ + public function GetAllKeys(): array { + if (!CurrentUser::hasAnyPermissions('administrator')) { + throw new UnauthorizedException(); + } + + return Key::findAll(); + } + + /** + * @permission administrator|user + * + * @param int $keyid + * + * @return \app\domain\Key + */ + public function GetKey($keyid): Key { + return $this->getKeyById($keyid); + } + + /** + * @permission administrator + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key[] + */ + public function GetSystemKeys(): array { + if (!CurrentUser::hasAnyPermissions('administrator')) { + throw new UnauthorizedException(); + } + + // TODO implement proper typing + // @phpstan-ignore property.notFound + return Key::where(Where::Null(Key::Schema()->Columns()->userid)); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param string[] $types + * + * @return \app\domain\Key[] + */ + public function GetUserKeys($userid, $types): array { + if (CurrentUser::getIdentity() == $userid || CurrentUser::hasAnyPermissions('administrator')) { + $user = User::find($userid); + if ($user != null) { + $keys = []; + foreach ($user->keys->all() as $key) { + if (in_array($key->type, $types)) { + array_push($keys, $key); + } + } + + return $keys; + } + } + + return []; + } + + /** + * @permission administrator|user + * + * @param int $keyid + * @param string|string[] $privileges + * + * @return bool + */ + public function PutKeyPrivileges($keyid, $privileges): bool { + $key = $this->getKeyById($keyid); + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + // TODO fix typing + /** @disregard P1006 override */ + foreach ($key->privileges->all() as $priv) { + // TODO fix typing + /** @disregard P1006 override */ + $key->privileges->remove($priv); + } + + foreach ($privs as $priv) { + // TODO fix typing + /** @disregard P1006 override */ + $key->privileges->add($priv); + } + + return $key->save(); + } + + /** + * @permission administrator|user + * + * @param int $keyid + * @param string $notes + * @param string $expires + * + * @return bool + */ + public function UpdateKey($keyid, $notes, $expires): bool { + $key = $this->getKeyById($keyid); + + $key->notes = $notes; + $key->expires = $expires; + + return $key->save(); + } + + /** + * Summary of getKeyById. + * + * @param mixed $keyid + * + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key + */ + private function getKeyById($keyid): Key { + /** @var \app\domain\Key|null */ + $key = Key::find($keyid); + + if (is_null($key)) { + throw new InvalidInputException('Invalid keyid'); + } + + if (!CurrentUser::hasAnyPermissions('administrator')) { + if (is_null($key->userid) || $key->userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + } + + return $key; + } +} diff --git a/packages/backend-php/app/handlers/v2/MemberCardServiceHandler2.php b/packages/backend-php/app/handlers/v2/MemberCardServiceHandler2.php new file mode 100644 index 000000000..5a39ebc8e --- /dev/null +++ b/packages/backend-php/app/handlers/v2/MemberCardServiceHandler2.php @@ -0,0 +1,289 @@ +addUserIDToFilters($userid, $filters); + + return GenuineCard::count($filters); + } + + /** + * @permission administrator + * + * @param string $key + * + * @return \app\domain\GenuineCard + */ + public function GetGenuineCardDetails($key): GenuineCard { + return GenuineCard::findByKey($key)[0]; + } + + /** + * @permission administrator + * + * @param string $email + * @param string $key + * + * @throws \app\exceptions\InvalidInputException + * @throws \app\exceptions\MemberCardException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\GenuineCard + */ + public function IssueCard($email, $key): GenuineCard { + $users = User::findByPaymentEmail($email); + + if (is_null($users) || count($users) != 1) { + throw new InvalidInputException('Invalid email address'); + } + + if (!$this->ValidateGenuineCard($key)) { + throw new InvalidInputException('Invalid card'); + } + + $user = $users[0]; + $card = GenuineCard::findByKey($key)[0]; + + $payments = Payment::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->status, 1), + // TODO implement proper typing + // @phpstan-ignore property.notFound + // TODO eventually put these into card campaigns or something + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->item_number, 'vhs_card_2015'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->payer_email, $email), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->user_id, $user->id), + Where::NotIn( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Payment::Schema()->Columns()->id, + Query::Select( + GenuineCard::Schema()->Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + new Columns(GenuineCard::Schema()->Columns()->paymentid) + ) + ) + ) + ); + + if (is_null($payments) || count($payments) < 1) { + throw new MemberCardException('User has not paid for a member card.', HttpStatusCodes::Client_Error_Payment_Required); + } + + $payment = $payments[0]; + + $card->paymentid = $payment->id; + $card->active = true; + $card->userid = $user->id; + $card->owneremail = $email; + $card->issued = date('Y-m-d H:i:s'); + $card->notes = 'Issued by admin to ' . $user->fname . ' ' . $user->lname; + + $card->save(); + + $keyService2 = new KeyServiceHandler2(); + + $keyService2->GenerateUserKey($user->id, 'rfid', $key, 'Genuine VHS Membership Card'); + + return $card; + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\GenuineCard[] + */ + public function ListGenuineCards($page, $size, $columns, $order, $filters): array { + /** @var \app\domain\GenuineCard[] */ + return GenuineCard::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\GenuineCard[] + */ + public function ListUserGenuineCards($userid, $page, $size, $columns, $order, $filters): array { + $userService2 = new UserServiceHandler2(); + $user = $userService2->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::_Or(Filter::Equal('userid', $user->id), Filter::Equal('owneremail', $user->email)); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + /** @var \app\domain\GenuineCard[] */ + return GenuineCard::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param string $key + * @param string $notes + * + * @throws \app\exceptions\MemberCardException + * + * @return \app\domain\GenuineCard + */ + public function RegisterGenuineCard($key, $notes): GenuineCard { + $keys = GenuineCard::findByKey($key); + + if (!is_null($keys) && count($keys) != 0) { + //card already registered + throw new MemberCardException('Failed to register card'); + } + + $card = new GenuineCard(); + + $card->key = $key; + + $card->save(); + + return $card; + } + + /** + * @permission administrator + * + * @param string $key + * @param bool $active + * + * @throws \app\exceptions\InvalidInputException + * + * @return bool + */ + public function UpdateGenuineCardActive($key, $active): bool { + if (!$this->ValidateGenuineCard($key)) { + throw new InvalidInputException('Invalid card'); + } + + /** @var GenuineCard */ + $card = GenuineCard::findByKey($key)[0]; + + $card->active = $active; + + return $card->save(); + } + + /** + * @permission authenticated + * + * @param string $key + * + * @return bool + */ + public function ValidateGenuineCard($key): bool { + $keys = GenuineCard::findByKey($key); + + return !is_null($keys) && count($keys) == 1; + } + + /** + * addUserIDToFilters. + * + * @param int $userid + * @param Filter|string $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Filter + */ + private function addUserIDToFilters($userid, $filters) { + $userService2 = new UserServiceHandler2(); + $user = $userService2->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::Equal('userid', $user->id); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + return $filters; + } +} diff --git a/packages/backend-php/app/handlers/v2/MembershipServiceHandler2.php b/packages/backend-php/app/handlers/v2/MembershipServiceHandler2.php new file mode 100644 index 000000000..733359507 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/MembershipServiceHandler2.php @@ -0,0 +1,221 @@ +getMembershipById($membershipId); + } + + /** + * @permission administrator + * + * @return \app\domain\Membership[] + */ + public function GetAll(): array { + return Membership::findAll(); + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\Membership[] + */ + public function ListMemberships($page, $size, $columns, $order, $filters): array { + /** @var \app\domain\Membership[] */ + return Membership::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param string|string[] $privileges + * + * @return bool + */ + public function PutPrivileges($membershipId, $privileges): bool { + $membership = $this->getMembershipById($membershipId); + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + // TODO fix typing + /** @disregard P1006 PHP0404 override */ + foreach ($membership->privileges->all() as $priv) { + // TODO fix typing + /** @disregard P1006 PHP0404 override */ + $membership->privileges->remove($priv); + } + + foreach ($privs as $priv) { + // TODO fix typing + /** @disregard P1006 PHP0404 override */ + $membership->privileges->add($priv); + } + + return $membership->save(); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param string $title + * @param string $description + * @param int $price + * @param string $code + * @param int $days + * @param string $period + * + * @return bool + */ + public function Update($membershipId, $title, $description, $price, $code, $days, $period): bool { + $membership = $this->getMembershipById($membershipId); + + $membership->title = $title; + $membership->description = $description; + $membership->price = $price; + $membership->code = $code; + $membership->days = $days; + $membership->period = $period; + + return $membership->save(); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param bool $active + * + * @return bool + */ + public function UpdateActive($membershipId, $active): bool { + $membership = $this->getMembershipById($membershipId); + + $membership->active = $active; + + return $membership->save(); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param bool $privateVal + * + * @return bool + */ + public function UpdatePrivate($membershipId, $privateVal): bool { + $membership = $this->getMembershipById($membershipId); + + $membership->private = $privateVal; + + return $membership->save(); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param bool $recurring + * + * @return bool + */ + public function UpdateRecurring($membershipId, $recurring): bool { + $membership = $this->getMembershipById($membershipId); + + $membership->recurring = $recurring; + + return $membership->save(); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param bool $trial + * + * @return bool + */ + public function UpdateTrial($membershipId, $trial): bool { + $membership = $this->getMembershipById($membershipId); + + $membership->trial = $trial; + + return $membership->save(); + } + + /** + * getMembershipById. + * + * @param int $membershipId + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return Membership + */ + private function getMembershipById($membershipId): Membership { + /** @var Membership|null */ + $membership = Membership::find($membershipId); + + if (is_null($membership)) { + throw new DomainException(sprintf('Missing membership for queried id: [%s]', $membershipId)); + } + + return $membership; + } +} diff --git a/packages/backend-php/app/handlers/v2/MetricServiceHandler2.php b/packages/backend-php/app/handlers/v2/MetricServiceHandler2.php new file mode 100644 index 000000000..5cf8d64b9 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/MetricServiceHandler2.php @@ -0,0 +1,485 @@ +Columns()->created, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::LesserEqual(User::Schema()->Columns()->created, $end_range) + ) + ); + + $byDowHour = []; + + $byMonthDow = []; + + foreach ($users as $user) { + $date = new DateTime($user->created); + + $dow = $date->format('w'); + $hour = $date->format('G'); + $month = $date->format('n'); + + if (!array_key_exists($dow, $byDowHour)) { + $byDowHour[$dow] = []; + $byDowHour[$dow]['total'] = 0; + } + + $byDowHour[$dow]['total'] += 1; + + if (!array_key_exists($hour, $byDowHour[$dow])) { + $byDowHour[$dow][$hour] = 0; + } + + $byDowHour[$dow][$hour] += 1; + + if (!array_key_exists($month, $byMonthDow)) { + $byMonthDow[$month] = []; + $byMonthDow[$month]['total'] = 0; + } + + $byMonthDow[$month]['total'] += 1; + + if (!array_key_exists($dow, $byMonthDow[$month])) { + $byMonthDow[$month][$dow] = 0; + } + + $byMonthDow[$month][$dow] += 1; + } + + $result = new MetricServiceGetCreatedDatesResult([ + 'start_range' => $start_range, + 'end_range' => $end_range, + 'byDowHour' => $byDowHour, + 'byMonthDow' => $byMonthDow + ]); + + return $result; + } + + /** + * @permission administrator + * + * @return \app\domain\Payment[] + */ + public function GetExceptionPayments(): array { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return Payment::where(Where::NotEqual(Payment::Schema()->Columns()->status, 1)); + } + + /** + * @permission user + * + * @param string $start_range + * @param string $end_range + * @param \app\enums\MetricServiceGroupType $group + * + * @return \app\dto\v2\MetricServiceGetMembersResult + */ + public function GetMembers($start_range, $end_range, $group): MetricServiceGetMembersResult { + $users = User::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(User::Schema()->Columns()->created, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::LesserEqual(User::Schema()->Columns()->created, $end_range) + ) + ); + + $payments = Payment::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->status, 1), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(Payment::Schema()->Columns()->date, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::LesserEqual(Payment::Schema()->Columns()->date, $end_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Like(Payment::Schema()->Columns()->item_number, 'vhs_membership_%') + ) + ); + + $created = []; + $expired = []; + + foreach ($users as $user) { + $created[] = $this->countByDate($created, $user->created, $group); + $expired[] = $this->countByDate($expired, $user->mem_expire, $group); + } + + $total = []; + + foreach ($payments as $payment) { + $total = $this->countByDate($total, $payment->date, $group); + } + + ksort($created); + ksort($expired); + ksort($total); + + $result = new MetricServiceGetMembersResult([ + 'start_range' => $start_range, + 'end_range' => $end_range, + 'created' => $created, + 'expired' => $expired, + 'total' => $total + ]); + + return $result; + } + + /** + * @permission user + * + * @param string $start_range string iso date in UTC, if empty is start of today + * @param string $end_range string iso date in UTC, if empty is end of today + * + * @return \app\dto\v2\MetricServiceNewKeyholdersResult + */ + public function GetNewKeyHolders($start_range, $end_range): MetricServiceNewKeyholdersResult { + $start = strtotime($start_range); + $end = strtotime($end_range); + $membership = Membership::findByCode(Membership::KEYHOLDER); + $count = $this->NewMembershipByIdCount($membership[0]->id, $start, $end); + + return new MetricServiceNewKeyholdersResult([ + 'value' => $count + ]); + } + + /** + * @permission user + * + * @param string $start_range string iso date in UTC, if empty is start of today + * @param string $end_range string iso date in UTC, if empty is end of today + * + * @return \app\dto\v2\MetricServiceNewMembersResult + */ + public function GetNewMembers($start_range, $end_range): MetricServiceNewMembersResult { + $start = strtotime($start_range); + $end = strtotime($end_range); + $count = $this->NewMemberCount($start, $end); + + return new MetricServiceNewMembersResult([ + 'start_range' => $start_range, + 'start' => $start, + 'end_range' => $end_range, + 'end' => $end, + 'value' => $count + ]); + } + + /** + * @permission administrator + * + * @return \app\domain\User[] + */ + public function GetPendingAccounts(): mixed { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return User::where(Where::Equal(User::Schema()->Columns()->active, UserActiveEnum::PENDING->value)); + } + + /** + * @permission user + * + * @param string $start_range string iso date in UTC, if empty is end of today + * @param string $end_range string iso date in UTC, if empty is end of today + * @param \app\enums\MetricServiceGroupType $group group by month, day, year + * + * @return \app\dto\v2\MetricServiceGetRevenueResult + */ + public function GetRevenue($start_range, $end_range, $group): MetricServiceGetRevenueResult { + $payments = Payment::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->status, 1), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(Payment::Schema()->Columns()->date, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::LesserEqual(Payment::Schema()->Columns()->date, $end_range) + ) + ); + + $byMembership = []; + $byDate = []; + + foreach ($payments as $payment) { + $membershipKey = $payment->item_number == '' || is_null($payment->item_number) ? 'Donation' : $payment->item_number; + if (!array_key_exists($membershipKey, $byMembership)) { + $byMembership[$membershipKey] = []; + } + + $grouping = new DateTime($payment->date); + + switch ($group) { + case 'day': + $grouping = $grouping->format('Y-m-d'); + + break; + case 'month': + $grouping = $grouping->format('Y-m'); + + break; + case 'year': + $grouping = $grouping->format('Y'); + + break; + default: + $grouping = 'all'; + + break; + } + + if (!array_key_exists($grouping, $byMembership[$membershipKey])) { + $byMembership[$membershipKey][$grouping] = 0; + } + + $byMembership[$membershipKey][$grouping] += $payment->rate_amount; + + if (!array_key_exists($grouping, $byDate)) { + $byDate[$grouping] = 0; + } + + $byDate[$grouping] += $payment->rate_amount; + } + + return new MetricServiceGetRevenueResult([ + 'start_range' => $start_range, + 'end_range' => $end_range, + 'grouping' => $byDate, + 'by_membership' => $byMembership + ]); + } + + /** + * @permission user + * + * @return \app\dto\v2\MetricServiceTotalKeyHoldersResult + */ + public function GetTotalKeyHolders(): MetricServiceTotalKeyHoldersResult { + $membership = Membership::findByCode(Membership::KEYHOLDER); + $count = $this->TotalMembershipByIdCount($membership[0]->id); + + return new MetricServiceTotalKeyHoldersResult([ + 'value' => $count + ]); + } + + /** + * @permission user + * + * @return \app\dto\v2\MetricServiceTotalMembersResult + */ + public function GetTotalMembers(): MetricServiceTotalMembersResult { + $count = $this->TotalMemberCount(); + + return new MetricServiceTotalMembersResult([ + 'value' => $count + ]); + } + + /** + * countByDate. + * + * @param mixed[] $arr + * @param mixed $date + * @param mixed $group + * + * @return mixed[] + */ + private function countByDate($arr, $date, $group): mixed { + if (is_null($date)) { + return $arr; + } + + $grouping = new DateTime($date); + + if ($grouping > new DateTime()) { + return $arr; + } + + switch ($group) { + case 'day': + $grouping = $grouping->format('Y-m-d'); + + break; + case 'month': + $grouping = $grouping->format('Y-m'); + + break; + case 'year': + $grouping = $grouping->format('Y'); + + break; + default: + $grouping = 'all'; + + break; + } + + if (!array_key_exists($grouping, $arr)) { + $arr[$grouping] = 0; + } + + $arr[$grouping] += 1; + + return $arr; + } + + /** + * Get the total new members recorded in the date range. + * + * @param int $start int unixtime + * @param int $end int unixtime + * + * @return int + */ + private function NewMemberCount($start, $end): int { + $query = Query::count( + UserSchema::Table(), + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(UserSchema::Columns()->active, UserActiveEnum::ACTIVE->value), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(UserSchema::Columns()->mem_expire, date(Formats::DATE_TIME_ISO_SHORT_FULL)), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::LesserEqual(UserSchema::Columns()->created, date(Formats::DATE_TIME_ISO_SHORT_MIDNIGHT, $end)), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(UserSchema::Columns()->created, date(Formats::DATE_TIME_ISO_SHORT_MIDNIGHT, $start)) + ) + ); + + return Database::count($query); + } + + /** + * Get the total new memberships of a type recorded in the date range. + * + * @param int $membership_id int + * @param int $start int unixtime + * @param int $end int unixtime + * + * @return int + */ + private function NewMembershipByIdCount($membership_id, $start, $end): int { + $query = Query::count( + UserSchema::Table(), + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(UserSchema::Columns()->active, UserActiveEnum::ACTIVE->value), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(UserSchema::Columns()->mem_expire, date(Formats::DATE_TIME_ISO_SHORT_FULL)), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(UserSchema::Columns()->membership_id, $membership_id), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::LesserEqual(UserSchema::Columns()->created, date(Formats::DATE_TIME_ISO_SHORT_MIDNIGHT, $end)), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(UserSchema::Columns()->created, date(Formats::DATE_TIME_ISO_SHORT_MIDNIGHT, $start)) + ) + ); + + return Database::count($query); + } + + /** + * Get the total members. + * + * @return int + */ + private function TotalMemberCount(): int { + return Database::count( + Query::count( + UserSchema::Table(), + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(UserSchema::Columns()->active, UserActiveEnum::ACTIVE->value), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(UserSchema::Columns()->mem_expire, date(Formats::DATE_TIME_ISO_SHORT_FULL)) + ) + ) + ); + } + + /** + * TotalMembershipByIdCount. + * + * @param int $membership_id + * + * @return int + */ + private function TotalMembershipByIdCount($membership_id): int { + return Database::count( + Query::count( + UserSchema::Table(), + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(UserSchema::Columns()->active, UserActiveEnum::ACTIVE->value), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(UserSchema::Columns()->mem_expire, date(Formats::DATE_TIME_ISO_SHORT_FULL)), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(UserSchema::Columns()->membership_id, $membership_id) + ) + ) + ); + } +} diff --git a/packages/backend-php/app/handlers/v2/OAuthServiceHandler2.php b/packages/backend-php/app/handlers/v2/OAuthServiceHandler2.php new file mode 100644 index 000000000..737bdbc60 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/OAuthServiceHandler2.php @@ -0,0 +1,485 @@ +addUserIDToFilters($userid, $filters); + + return AppClient::count($filters); + } + + /** + * @permission administrator|user + * + * @param int $id + * + * @return void + */ + public function DeleteClient($id): void { + $client = $this->getOAuthClient($id); + + $client->delete(); + } + + /** + * @permission administrator|user + * + * @param int $id + * @param bool $enabled + * + * @return bool + */ + public function EnableClient($id, $enabled): bool { + $client = $this->getOAuthClient($id); + + $client->enabled = $enabled; + + return $client->save(); + } + + /** + * @permission oauth-provider + * + * @param string $bearerToken + * + * @return \app\domain\AccessToken + */ + public function GetAccessToken($bearerToken): AccessToken { + return AccessToken::findByToken($bearerToken); + } + + /** + * @permission anonymous + * + * @param int $clientId + * @param string $clientSecret + * + * @return \app\dto\TrimmedAppClient|null + */ + public function GetClient($clientId, $clientSecret): TrimmedAppClient|null { + $client = AppClient::find($clientId); + + if (!is_null($client) && $client->secret == $clientSecret) { + return $this->trimClient($client); + } + + return null; + } + + /** + * GetClientDetails. + * + * @permission administrator|user + * + * @param int $id + * + * @return \app\domain\AppClient + */ + public function GetClientDetails($id): AppClient { + return $this->getOAuthClient($id); + } + + /** + * @permission oauth-provider + * @permission authenticated + * + * @param int $clientId + * + * @return \app\dto\TrimmedAppClient|null + */ + public function GetClientInfo($clientId): TrimmedAppClient|null { + $client = AppClient::find($clientId); + + if (!is_null($client)) { + return $this->trimClientInfo($client); + } + + return null; + } + + /** + * @permission oauth-provider + * + * @param string $refreshToken + * + * @return \app\domain\RefreshToken + */ + public function GetRefreshToken($refreshToken): RefreshToken { + return RefreshToken::findByToken($refreshToken); + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\AppClient[] + */ + public function ListClients($page, $size, $columns, $order, $filters): array { + return AppClient::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\AppClient[] + */ + public function ListUserClients($userid, $page, $size, $columns, $order, $filters): array { + $userService2 = new UserServiceHandler2(); + $user = $userService2->GetUser($userid); + + AppClient::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::Equal('userid', $user->id); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + $cols = explode(',', $columns); + + array_push($cols, 'userid'); + + $columns = implode(',', array_unique($cols)); + + return AppClient::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param string $name + * @param string $description + * @param string $url + * @param string $redirecturi + * + * @return \app\domain\AppClient + */ + public function RegisterClient($name, $description, $url, $redirecturi): AppClient { + $client = new AppClient(); + + $client->name = $name; + $client->description = $description; + $client->url = $url; + $client->redirecturi = $redirecturi; + $client->secret = bin2hex(openssl_random_pseudo_bytes(32)); + $client->owner = User::find(CurrentUser::getIdentity()); + $client->expires = date('Y-m-d', strtotime('+1 year')); + + if (!$client->save()) { + throw new HttpException('Failed to save new client!', HttpStatusCodes::Server_Error_Internal_Service_Error); + } + + return $client; + } + + /** + * @permission oauth-provider + * + * @param string $refreshToken + * + * @throws \vhs\exceptions\HttpException + * + * @return void + */ + public function RevokeRefreshToken($refreshToken): void { + $token = RefreshToken::findByToken($refreshToken); + + if (is_null($token)) { + throw new HttpException('RefreshToken token not found', HttpStatusCodes::Client_Error_Not_Found); + } + + $token->delete(); + } + + /** + * @permission oauth-provider + * + * @param int $userId + * @param string $accessToken + * @param int $clientId + * @param string $expires + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\domain\User|false + */ + public function SaveAccessToken($userId, $accessToken, $clientId, $expires): User|false { + $user = User::find($userId); + + if (is_null($user)) { + throw new HttpException('User not found', HttpStatusCodes::Client_Error_Not_Found); + } + + $token = new AccessToken(); + + $token->token = $accessToken; + $token->user = $user; + + $client = AppClient::find($clientId); + + if (!is_null($client)) { + $token->client = $client; + } + + $expiry = new DateTime($expires); + + $token->expires = $expiry->format('Y-m-d H:i:s'); + + $token->save(); + + return $user; + } + + /** + * @permission oauth-provider + * + * @param int $userId + * @param string $refreshToken + * @param int $clientId + * @param string $expires + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\dto\TrimmedUser + */ + public function SaveRefreshToken($userId, $refreshToken, $clientId, $expires): TrimmedUser { + $user = User::find($userId); + $client = AppClient::find($clientId); + + if (is_null($user)) { + throw new HttpException('User not found', HttpStatusCodes::Client_Error_Not_Found); + } + + $token = new RefreshToken(); + + $token->token = $refreshToken; + $token->user = $user; + + if (!is_null($client)) { + $token->client = $client; + } + + $expiry = new DateTime($expires); + + $token->expires = $expiry->format('Y-m-d H:i:s'); + + $token->save(); + + return $this->trimUser($user); + } + + /** + * @permission administrator|user + * + * @param int $id + * @param string $name + * @param string $description + * @param string $url + * @param string $redirecturi + * + * @throws \vhs\exceptions\HttpException + * + * @return bool + */ + public function UpdateClient($id, $name, $description, $url, $redirecturi): bool { + $client = $this->getOAuthClient($id); + + $client->name = $name; + $client->description = $description; + $client->url = $url; + $client->redirecturi = $redirecturi; + + return $client->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $expires + * + * @throws \vhs\exceptions\HttpException + * + * @return bool + */ + public function UpdateClientExpiry($id, $expires): bool { + $client = AppClient::find($id); + + if (is_null($client)) { + throw new HttpException('Client not found', HttpStatusCodes::Client_Error_Not_Found); + } + + $client->expires = $expires; + + return $client->save(); + } + + /** + * Summary of AddUserIDToFilters. + * + * @param mixed $userid + * @param string|\vhs\domain\Filter|null $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \vhs\domain\Filter + */ + private function addUserIDToFilters($userid, $filters): Filter { + $userService2 = new UserServiceHandler2(); + $user = $userService2->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::Equal('userid', $user->id); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + return $filters; + } + + /** + * getOAuthClient. + * + * @param int $id + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\domain\AppClient + */ + private function getOAuthClient($id): AppClient { + $client = AppClient::find($id); + + if (is_null($client)) { + throw new HttpException('Client not found', HttpStatusCodes::Client_Error_Not_Found); + } + + if (CurrentUser::getIdentity() != $client->userid && !CurrentUser::hasAnyPermissions('administrator')) { + throw new HttpException('Client is not accessible', HttpStatusCodes::Client_Error_Forbidden); + } + + return $client; + } + + /** + * Summary of trimClient. + * + * @param \app\domain\AppClient $client + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\dto\TrimmedAppClient + */ + private function trimClient($client): TrimmedAppClient { + if (is_null($client)) { + throw new HttpException('Client not found', HttpStatusCodes::Client_Error_Not_Found); + } + + return new TrimmedAppClient($client); + } + + /** + * Summary of trimClientInfo. + * + * @param \app\domain\AppClient|null $client + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\dto\TrimmedAppClient + */ + private function trimClientInfo($client): TrimmedAppClient { + if (is_null($client)) { + throw new HttpException('Client not found', HttpStatusCodes::Client_Error_Not_Found); + } + + return new TrimmedAppClient($client); + } + + /** + * Summary of trimUser. + * + * @param \app\domain\User|null $user + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\dto\TrimmedUser + */ + private function trimUser($user): TrimmedUser { + if (is_null($user)) { + throw new HttpException('Client not found', HttpStatusCodes::Client_Error_Not_Found); + } + + return new TrimmedUser($user); + } +} diff --git a/packages/backend-php/app/handlers/v2/PaymentServiceHandler2.php b/packages/backend-php/app/handlers/v2/PaymentServiceHandler2.php new file mode 100644 index 000000000..3ea4f7842 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/PaymentServiceHandler2.php @@ -0,0 +1,154 @@ +addUserIDOrEMailToFilters($userid, $filters); + + return Payment::count($filters); + } + + /** + * @permission administrator|user + * + * @param int $id + * + * @return \app\domain\Payment|null + */ + public function GetPayment($id): Payment|null { + /** @var Payment|null */ + $payment = Payment::find($id); + + if (is_null($payment)) { + return null; + } + + if (CurrentUser::getIdentity() == $payment->user_id || CurrentUser::hasAnyPermissions('administrator')) { + return $payment; + } + + return null; + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\Payment[] + */ + public function ListPayments($page, $size, $columns, $order, $filters): array { + return Payment::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\Payment[] + */ + public function ListUserPayments($userid, $page, $size, $columns, $order, $filters): array { + $filters = $this->addUserIDOrEMailToFilters($userid, $filters); + + return Payment::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param int $paymentid + * + * @return string + */ + public function ReplayPaymentProcessing($paymentid): string { + $log = new StringLogger(); + + $log->log('Attempting a reply of payment id: ' . $paymentid); + + $processor = new PaymentProcessor($log); + + try { + $processor->paymentCreated($paymentid); + } catch (\Exception $ex) { + $log->log('Exception: ' . $ex->getMessage()); + $log->log($ex->getTraceAsString()); + } + + $log->log('Replay complete.'); + + // @phpstan-ignore method.notFound + return $log->fullText(); + } + + /** + * addUserIDOrEMailToFilters. + * + * @param int $userid + * @param Filter|string $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Filter + */ + private function addUserIDOrEMailToFilters($userid, $filters): Filter { + $userService2 = new UserServiceHandler2(); + $user = $userService2->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::_Or(Filter::Equal('user_id', $user->id), Filter::Equal('payer_email', $user->email)); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + return $filters; + } +} diff --git a/packages/backend-php/app/handlers/v2/PinServiceHandler2.php b/packages/backend-php/app/handlers/v2/PinServiceHandler2.php new file mode 100644 index 000000000..5f3450796 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/PinServiceHandler2.php @@ -0,0 +1,227 @@ +getUserPinByUserId($userid); + + if (is_null($pin)) { + $nextpinid = Database::scalar( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Query::Select(SettingsSchema::Table(), new Columns(SettingsSchema::Columns()->nextpinid)) + ); + + $key = new Key(); + $key->userid = $userid; + $key->type = 'pin'; + $key->key = sprintf('%04s', $nextpinid) . '|' . sprintf('%04s', rand(0, 9999)); + $key->notes = 'User generated PIN'; + + $pin = $key; + + $priv = Privilege::findByCode('inherit'); + if (!is_null($priv)) { + // TODO fix typing + /** @disregard P1006 override */ + $pin->privileges->add($priv); + } + } + + $pinid = explode('|', $pin->key)[0]; + + $pin->key = sprintf('%04s', $pinid) . '|' . sprintf('%04s', rand(0, 9999)); + $pin->notes = 'User generated PIN'; + + $pin->save(); + + return $pin; + } + + /** + * @permission gen-temp-pin|administrator + * + * @param string $expires + * @param string $privileges + * @param string $notes + * + * @return \app\domain\Key + */ + public function GenerateTemporaryPin($expires, $privileges, $notes): Key { + $userid = CurrentUser::getIdentity(); + + $nextpinid = Database::scalar( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Query::Select(SettingsSchema::Table(), new Columns(SettingsSchema::Columns()->nextpinid)) + ); + + $pin = new Key(); + $pin->userid = $userid; + $pin->expires = $expires; + $pin->type = 'pin'; + $pin->key = sprintf('%04s', $nextpinid) . '|' . sprintf('%04s', rand(0, 9999)); + $pin->notes = $notes; + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + if (!is_null($privs) && is_array($privs)) { + foreach ($privs as $priv) { + if (CurrentUser::hasAllPermissions($priv->code)) { + // TODO fix typing + /** @disregard P1006 override */ + $pin->privileges->add($priv); + } + } + } + + $pin->save(); + + return $pin; + } + + /** + * @permission administrator|user + * + * @param int $userid + * + * @return \app\domain\Key|null + */ + public function GetUserPin($userid): Key|null { + return $this->getUserPinByUserId($userid); + } + + /** + * @permission administrator|user + * + * @param int $keyid + * @param string $pin + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return bool + */ + public function UpdatePin($keyid, $pin): bool { + /** @var \app\domain\Key */ + $key = Key::find($keyid); + + if (!CurrentUser::hasAnyPermissions('administrator') && $key->userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + $pinid = explode('|', $key->key)[0]; + + $key->key = $pinid . '|' . sprintf('%04s', intval($pin)); + + return $key->save(); + } + + /** + * Change a pin. + * + * @permission administrator|user + * + * @param int $userid + * @param string $pin + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return bool + */ + public function UpdateUserPin($userid, $pin): bool { + if (!CurrentUser::hasAnyPermissions('administrator') && $userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + $pinObj = $this->getUserPinByUserId($userid); + + if (is_null($pin)) { + $pinObj = $this->GeneratePin($userid); + } + + $pinid = explode('|', $pinObj->key)[0]; + + $pinObj->key = $pinid . '|' . $pin; + + return $pinObj->save(); + } + + /** + * Summary of getUserPinByUserId. + * + * @param mixed $userid + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key|null + */ + private function getUserPinByUserId($userid): ?Key { + if (!CurrentUser::hasAnyPermissions('administrator') && $userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + $keys = Key::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Key::Schema()->Columns()->type, 'pin'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Key::Schema()->Columns()->userid, $userid) + ) + ); + + return !empty($keys) ? $keys[0] : null; + } +} diff --git a/packages/backend-php/app/handlers/v2/PreferenceServiceHandler2.php b/packages/backend-php/app/handlers/v2/PreferenceServiceHandler2.php new file mode 100644 index 000000000..f70824792 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/PreferenceServiceHandler2.php @@ -0,0 +1,239 @@ +delete(); + } + } + + /** + * @permission administrator + * + * @return \app\domain\SystemPreference[] + */ + public function GetAllSystemPreferences(): array { + return SystemPreference::findAll(); + } + + /** + * @permission administrator + * + * @param int $id + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return \app\domain\SystemPreference + */ + public function GetSystemPreference($id): SystemPreference { + /** @var SystemPreference|null */ + $systemPreference = SystemPreference::findByKey($id); + if (is_null($systemPreference) || count($systemPreference) <= 0) { + throw new DomainException(sprintf('SystemPreference with id [%s] not found!', $id)); + } + + return $systemPreference[0]; + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\SystemPreference[] + */ + public function ListSystemPreferences($page, $size, $columns, $order, $filters): array { + /** @var SystemPreference[] */ + return SystemPreference::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param string $key + * @param string $value + * @param bool $enabled + * @param string $notes + * + * @return \app\domain\SystemPreference + */ + public function PutSystemPreference($key, $value, $enabled, $notes): SystemPreference { + $prefs = SystemPreference::findByKey($key); + + $pref = null; + + if (count($prefs) != 1) { + $pref = new SystemPreference(); + } else { + $pref = $prefs[0]; + } + + $pref->key = $key; + $pref->value = $value; + $pref->enabled = $enabled; + $pref->notes = $notes; + + $pref->save(); + + return $pref; + } + + /** + * @permission administrator + * + * @param int $id + * @param string|string[] $privileges + * + * @return bool + */ + public function PutSystemPreferencePrivileges($id, $privileges): bool { + /** @var \app\domain\SystemPreference */ + $pref = SystemPreference::find($id); + + if (is_null($pref)) { + return false; + } + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + foreach ($pref->privileges->all() as $priv) { + $pref->privileges->remove($priv); + } + + foreach ($privs as $priv) { + $pref->privileges->add($priv); + } + + return $pref->save(); + } + + /** + * @permission anonymous + * + * @param string $key + * + * @return \app\domain\SystemPreference|null + */ + public function SystemPreference($key): SystemPreference|null { + $prefs = SystemPreference::findByKey($key, function ($privileges) { + $codes = []; + foreach ($privileges->all() as $priv) { + array_push($codes, $priv->code); + } + + return CurrentUser::hasAllPermissions(...$codes); + }); + + if (count($prefs) != 1) { + return null; + } + + return $prefs[0]; + } + + /** + * @permission administrator + * + * @param int $id + * @param string $key + * @param string $value + * @param bool $enabled + * @param string $notes + * + * @throws \vhs\exceptions\HttpException + * + * @return bool + */ + public function UpdateSystemPreference($id, $key, $value, $enabled, $notes): bool { + /** @var \app\domain\SystemPreference */ + $pref = SystemPreference::find($id); + + if (is_null($pref)) { + throw new HttpException('SystemPreference not found', HttpStatusCodes::Client_Error_Not_Found); + } + + $pref->key = $key; + $pref->value = $value; + $pref->enabled = $enabled; + $pref->notes = $notes; + + return $pref->save(); + } + + /** + * @permission administrator + * + * @param string $key + * @param bool $enabled + * + * @throws \vhs\exceptions\HttpException + * + * @return bool + */ + public function UpdateSystemPreferenceEnabled($key, $enabled): bool { + /** @var \app\domain\SystemPreference[] */ + $prefs = SystemPreference::findByKey($key); + + $pref = null; + + if (count($prefs) != 1) { + throw new HttpException('SystemPreference not found', HttpStatusCodes::Client_Error_Not_Found); + } + + $pref = $prefs[0]; + + $pref->key = $key; + $pref->enabled = $enabled; + + return $pref->save(); + } +} diff --git a/packages/backend-php/app/handlers/v2/PrivilegeServiceHandler2.php b/packages/backend-php/app/handlers/v2/PrivilegeServiceHandler2.php new file mode 100644 index 000000000..97b76dd20 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/PrivilegeServiceHandler2.php @@ -0,0 +1,252 @@ +name = $name; + $priv->code = $code; + $priv->description = $description; + $priv->icon = $icon; + $priv->enabled = $enabled; + + $priv->save(); + + return $priv; + } + + /** + * @permission administrator + * + * @param int $id + * + * @return void + */ + public function DeletePrivilege($id): void { + /** @var \app\domain\Privilege */ + $priv = Privilege::find($id); + + $priv->delete(); + } + + /** + * @permission administrator|user|grants + * + * @return \app\domain\Privilege[] + */ + public function GetAllPrivileges(): array { + /** @var \app\domain\Privilege[] */ + return Privilege::findAll(); + } + + /** + * @permission administrator + * + * @return string[] + */ + public function GetAllSystemPermissions(): array { + $endpoints = ServiceRegistry::get('v2')->getAllEndpoints(); + + $flatPerms = []; + + /** @var Endpoint $endpoint */ + foreach ($endpoints as $endpoint) { + foreach ($endpoint->getAllPermissions() as $permissions) { + foreach ($permissions as $set) { + array_push($flatPerms, ...$set); + } + } + } + + $flatPerms = array_unique($flatPerms); + + $retval = []; + + foreach ($flatPerms as $perm) { + array_push($retval, $perm); + } + + return $retval; + } + + /** + * @permission user + * + * @param int $id + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return \app\domain\Privilege + */ + public function GetPrivilege($id): Privilege { + /** @var \app\domain\Privilege */ + $privilege = Privilege::find($id); + + if (is_null($privilege)) { + throw new DomainException(sprintf('Privilege with id [%s] not found!', $id), HttpStatusCodes::Client_Error_Not_Found->value); + } + + return $privilege; + } + + /** + * @permission administrator|user|grants + * + * @param int $userid + * + * @return \app\domain\Privilege[] + */ + public function GetUserPrivileges($userid): array { + $privileges = []; + $userService2 = new UserServiceHandler2($this->context); + + $user = $userService2->GetUser($userid); + + if (!is_null($user)) { + // TODO fix typing + /** @disregard P1006 override */ + foreach ($user->privileges->all() as $privilege) { + array_push($privileges, $privilege); + } + + foreach ($user->membership->privileges->all() as $privilege) { + array_push($privileges, $privilege); + } + } + + return $privileges; + } + + /** + * @permission administrator|user|grants + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\Privilege[] + */ + public function ListPrivileges($page, $size, $columns, $order, $filters): array { + /** @var Privilege[] */ + return Privilege::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $description + * + * @return bool + */ + public function UpdatePrivilegeDescription($id, $description): bool { + /** @var \app\domain\Privilege */ + $priv = Privilege::find($id); + + $priv->description = $description; + + return $priv->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param bool $enabled + * + * @return bool + */ + public function UpdatePrivilegeEnabled($id, $enabled): bool { + /** @var \app\domain\Privilege */ + $priv = Privilege::find($id); + + $priv->enabled = $enabled; + + return $priv->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $icon + * + * @return bool + */ + public function UpdatePrivilegeIcon($id, $icon): bool { + /** @var \app\domain\Privilege */ + $priv = Privilege::find($id); + + $priv->icon = $icon; + + return $priv->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $name + * + * @return bool + */ + public function UpdatePrivilegeName($id, $name): bool { + /** @var \app\domain\Privilege */ + $priv = Privilege::find($id); + + $priv->name = $name; + + return $priv->save(); + } +} diff --git a/packages/backend-php/app/handlers/v2/StripeEventServiceHandler2.php b/packages/backend-php/app/handlers/v2/StripeEventServiceHandler2.php new file mode 100644 index 000000000..9454e69cb --- /dev/null +++ b/packages/backend-php/app/handlers/v2/StripeEventServiceHandler2.php @@ -0,0 +1,72 @@ +value); + } + + return $stripeEvent; + } + + /** + * @permission administrator + * + * @return \app\domain\StripeEvent[] + */ + public function GetAll(): array { + return StripeEvent::findAll(); + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\StripeEvent[] + */ + public function ListRecords($page, $size, $columns, $order, $filters): array { + return StripeEvent::page($page, $size, $columns, $order, $filters); + } +} diff --git a/packages/backend-php/app/handlers/v2/SystemPreferenceServiceHandler2.php b/packages/backend-php/app/handlers/v2/SystemPreferenceServiceHandler2.php new file mode 100644 index 000000000..b533a500e --- /dev/null +++ b/packages/backend-php/app/handlers/v2/SystemPreferenceServiceHandler2.php @@ -0,0 +1,260 @@ +getSystemPreferencesByKey($keys); + + if (count($prefs) === 0) { + $this->throwNotFound(); + } + + foreach ($prefs as $pref) { + $pref->delete(); + } + } + + /** + * @permission administrator + * + * @return \app\domain\SystemPreference[] + */ + public function GetAllSystemPreferences(): array { + return SystemPreference::findAll(); + } + + /** + * @permission administrator + * + * @param int $id + * + * @return \app\domain\SystemPreference + */ + public function GetSystemPreference($id): SystemPreference { + return $this->getSystemPreferenceById($id); + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\SystemPreference[] + */ + public function ListSystemPreferences($page, $size, $columns, $order, $filters): array { + return SystemPreference::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param string $key + * @param string $value + * @param bool $enabled + * @param string $notes + * + * @return \app\domain\SystemPreference + */ + public function PutSystemPreference($key, $value, $enabled, $notes): SystemPreference { + $pref = null; + + try { + $pref = $this->getSystemPreferenceByKey($key); + } catch (\Exception $err) { + $pref = new SystemPreference(); + } + + $pref->key = $key; + $pref->value = $value; + $pref->enabled = $enabled; + $pref->notes = $notes; + + $pref->save(); + + return $pref; + } + + /** + * @permission administrator + * + * @param int $id + * @param string|string[] $privileges + * + * @return bool + */ + public function PutSystemPreferencePrivileges($id, $privileges): bool { + $pref = $this->getSystemPreferenceById($id); + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + foreach ($pref->privileges->all() as $priv) { + $pref->privileges->remove($priv); + } + + foreach ($privs as $priv) { + $pref->privileges->add($priv); + } + + return $pref->save(); + } + + /** + * @permission anonymous + * + * @param string $key + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\domain\SystemPreference + */ + public function SystemPreference($key): SystemPreference { + $prefs = SystemPreference::findByKey($key, function ($privileges) { + $codes = []; + foreach ($privileges->all() as $priv) { + array_push($codes, $priv->code); + } + + return CurrentUser::hasAllPermissions(...$codes); + }); + + if (count($prefs) != 1) { + throw new HttpException('Invalid SystemPreference result', HttpStatusCodes::Client_Error_Not_Found); + } + + return $prefs[0]; + } + + /** + * @permission administrator + * + * @param int $id + * @param string $key + * @param string $value + * @param bool $enabled + * @param string $notes + * + * @return bool + */ + public function UpdateSystemPreference($id, $key, $value, $enabled, $notes): bool { + $pref = $this->getSystemPreferenceById($id); + + $pref->key = $key; + $pref->value = $value; + $pref->enabled = $enabled; + $pref->notes = $notes; + + return $pref->save(); + } + + /** + * @permission administrator + * + * @param string $key + * @param bool $enabled + * + * @return bool + */ + public function UpdateSystemPreferenceEnabled($key, $enabled): bool { + $pref = $this->getSystemPreferenceByKey($key); + + $pref->key = $key; + $pref->enabled = $enabled; + + return $pref->save(); + } + + /** + * getSystemPreferenceById. + * + * @param int $id + * + * @return \app\domain\SystemPreference + */ + private function getSystemPreferenceById($id): SystemPreference { + /** @var SystemPreference|null */ + $pref = SystemPreference::find($id); + + if (is_null($pref)) { + $this->throwNotFound(); + } + + return $pref; + } + + /** + * getSystemPreferencesByKey. + * + * @param string $key + * + * @return \app\domain\SystemPreference + */ + private function getSystemPreferenceByKey($key): SystemPreference { + /** @var SystemPreference[]|null */ + $prefs = SystemPreference::findByKey($key); + + if (is_null($prefs) || count($prefs) !== 1) { + $this->throwNotFound(); + } + + return $prefs[0]; + } + + /** + * getSystemPreferencesByKey. + * + * @param string ...$keys + * + * @return \app\domain\SystemPreference[] + */ + private function getSystemPreferencesByKey(string ...$keys): array { + /** @var SystemPreference[]|null */ + $prefs = SystemPreference::findByKey(...$keys); + + if (is_null($prefs) || count($prefs) === 0) { + $this->throwNotFound(); + } + + return $prefs; + } +} diff --git a/packages/backend-php/app/handlers/v2/UserServiceHandler2.php b/packages/backend-php/app/handlers/v2/UserServiceHandler2.php new file mode 100644 index 000000000..e6b8d168f --- /dev/null +++ b/packages/backend-php/app/handlers/v2/UserServiceHandler2.php @@ -0,0 +1,803 @@ +AllowedColumns()); + } + + /** + * @permission administrator + * + * @param string $username + * @param string $password + * @param string $email + * @param string $fname + * @param string $lname + * @param int $membershipid + * + * @throws \app\exceptions\InvalidPasswordHashException + * @throws \app\exceptions\UserAlreadyExistsException + * + * @return \app\domain\User + */ + public function Create($username, $password, $email, $fname, $lname, $membershipid): User { + if (User::exists($username, $email)) { + throw new UserAlreadyExistsException(); + } + + $hashedPassword = PasswordUtil::hash($password); + + if ($hashedPassword === null) { + throw new InvalidPasswordHashException(); + } + + $user = new User(); + + $user->username = $username; + $user->password = $hashedPassword; + $user->email = $email; + $user->payment_email = $email; + $user->stripe_email = $email; + $user->fname = $fname; + $user->lname = $lname; + $user->active = UserActiveEnum::PENDING->value; + $user->token = bin2hex(openssl_random_pseudo_bytes(8)); + + $user->save(); + + try { + $this->UpdateMembership($user->id, $membershipid); + } catch (\Exception $ex) { + // Ignore result + } + + $protocol = + (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443 + ? StringLiterals::HTTPS_PREFIX + : StringLiterals::HTTP_PREFIX; + $domainName = $_SERVER['HTTP_HOST'] . '/'; + + EmailAdapter2::getInstance()->EmailUser($user, 'welcome', [ + 'token' => $user->token, + 'host' => $protocol . $domainName + ]); + + return $this->getUserById($user->id); + } + + /** + * @permission user|administrator + * + * @param int $userid + * + * @return bool + */ + public function GetStanding($userid): bool { + $user = $this->getUserById($userid); + + return new DateTime($user->mem_expire) > new DateTime(); + } + + /** + * @permission administrator + * + * @return array + * + * @phpstan-ignore missingType.iterableValue + */ + public function GetStatuses(): array { + return [ + ['title' => 'Active', 'code' => UserActiveEnum::ACTIVE->value], + ['title' => 'Pending', 'code' => UserActiveEnum::PENDING->value], + ['title' => 'Inactive', 'code' => UserActiveEnum::INACTIVE->value], + ['title' => 'Banned', 'code' => UserActiveEnum::BANNED->value] + ]; + } + + /** + * @permission administrator|user + * + * @param int $userid + * + * @return \app\domain\User|null + */ + public function GetUser($userid): User|null { + if (CurrentUser::getIdentity() == $userid || CurrentUser::hasAnyPermissions('administrator')) { + return $this->getUserById($userid); + } + + return null; + } + + /** + * Get the privileges that are grantable to the specified user by the user calling this service method. + * + * @permission grants + * + * @param int $userid + * + * @return array|array + */ + public function GetUserGrantablePrivileges($userid): array { + /** @var User $user */ + $user = $this->getUserById($userid); + + if (CurrentUser::canGrantAllPermissions('*')) { + return $user->getPrivilegeCodes(); + } + + $me = $this->getUserById(CurrentUser::getIdentity()); + + return array_intersect($user->getPrivilegeCodes(), $me->getGrantCodes()); + } + + /** + * @permission administrator + * + * @return \app\domain\User[] + */ + public function GetUsers(): array { + return User::findAll(); + } + + /** + * @permission grants + * + * @param int $userid + * @param string $privilege + * + * @throws \vhs\domain\exceptions\DomainException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return bool + */ + public function GrantPrivilege($userid, $privilege): bool { + if (!CurrentUser::canGrantAllPermissions($privilege)) { + throw new UnauthorizedException('Current user is not allowed to grant this privilege.'); + } + + /** @var User $user */ + $user = $this->getUserById($userid); + + if ($user === null) { + throw new DomainException('User not found'); + } + + /** @var Privilege $priv */ + $priv = Privilege::findByCode($privilege); + + if ($priv === null) { + throw new DomainException('Privilege not found'); + } + + // TODO fix typing + /** @disregard P1006 override */ + foreach ($user->privileges->all() as $p) { + if ($p->code == $priv->code) { + throw new DomainException('Privilege already granted'); + } + } + + // TODO fix typing + /** @disregard P1006 override */ + $user->privileges->add($priv); + + return $user->save(); + } + + /** + * @permission administrator|grants + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\User[] + */ + public function ListUsers($page, $size, $columns, $order, $filters): array { + return User::page($page, $size, $columns, $order, $filters, $this->AllowedColumns()); + } + + /** + * @permission administrator + * + * @param int $userid + * @param string $privileges + * + * @return bool + */ + public function PutUserPrivileges($userid, $privileges): bool { + $user = $this->getUserById($userid); + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + // TODO fix typing + /** @disregard P1006 override */ + foreach ($user->privileges->all() as $priv) { + // TODO fix typing + /** @disregard P1006 override */ + $user->privileges->remove($priv); + } + + foreach ($privs as $priv) { + // TODO fix typing + /** @disregard P1006 override */ + $user->privileges->add($priv); + } + + return $user->save(); + } + + /** + * @permission anonymous + * + * @param string $username + * @param string $password + * @param string $email + * @param string $fname + * @param string $lname + * + * @throws \app\exceptions\InvalidPasswordHashException + * @throws \app\exceptions\UserAlreadyExistsException + * + * @return \app\domain\User + */ + public function Register($username, $password, $email, $fname, $lname): User { + if (User::exists($username, $email)) { + throw new UserAlreadyExistsException(); + } + + $hashedPassword = PasswordUtil::hash($password); + + if ($hashedPassword === null) { + throw new InvalidPasswordHashException(); + } + + $user = new User(); + + $user->username = $username; + $user->password = $hashedPassword; + $user->email = $email; + $user->fname = $fname; + $user->lname = $lname; + $user->active = UserActiveEnum::PENDING->value; + $user->token = bin2hex(openssl_random_pseudo_bytes(8)); + + $user->save(); + + $protocol = + (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443 + ? StringLiterals::HTTPS_PREFIX + : StringLiterals::HTTP_PREFIX; + $domainName = $_SERVER['HTTP_HOST'] . '/'; + + EmailAdapter2::getInstance()->EmailUser($user, 'welcome', [ + 'token' => $user->token, + 'host' => $protocol . $domainName + ]); + + return $user; + } + + /** + * @permission anonymous + * + * @param string $email + * + * @return \app\dto\ServiceResponseError|\app\dto\ServiceResponseSuccess + */ + public function RequestPasswordReset($email): ServiceResponseSuccess|ServiceResponseError { + $user = User::findByEmail($email)[0]; + + if ($user === null) { + return new ServiceResponseErrorUserNotFoundByEmailAddress(); + } + + $request = new PasswordResetRequest(); + + $request->token = bin2hex(openssl_random_pseudo_bytes(8)); + $request->userid = $user->id; + + $request->save(); + + $protocol = + (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443 + ? StringLiterals::HTTPS_PREFIX + : StringLiterals::HTTP_PREFIX; + $domainName = $_SERVER['HTTP_HOST'] . '/'; + + EmailAdapter2::getInstance()->EmailUser($user, 'recover', [ + 'token' => $request->token, + 'host' => $protocol . $domainName + ]); + + return new ServiceResponseSuccess(); + } + + /** + * @permission user + * + * @param string $email + * + * @return bool|string|null + */ + public function RequestSlackInvite($email): bool|string|null { + $url = 'https://' . SLACK_URL . '/api/users.admin.invite'; + + $body = [ + 'email' => $email, + 'token' => SLACK_INVITE_TOKEN, + 'set_active' => true + ]; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($body)); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($ch, CURLOPT_FORBID_REUSE, true); + + $error = null; + $response = curl_exec($ch); + + if (!$response) { + $error = 'Error: Got ' . curl_error($ch) . " when request slack invite for email: '" . $email . "'"; + } + + curl_close($ch); + + if (!is_null($error)) { + // $this->context->log($error); + return $error; + } + + $json_data = json_decode($response, true); + // body looks like: + // {"ok":true} + // or + // {"ok":false,"error":"already_invited"} + + if ($json_data['ok']) { + return 'Invite sent'; + } + + $ErrorMap = [ + 'invalid_email' => 'The email you entered is an invalid email.', + 'invalid_auth' => 'Something has gone wrong. Please contact a system administrator.' + ]; + + $error = $json_data['error']; + + if ($error == 'already_invited' || $error == 'already_in_team') { + return 'Success! You were already invited. Visit ' . SLACK_URL; + } + + $error = $ErrorMap[$error] ?? $error; + + return $error; + } + + /** + * @permission anonymous + * + * @param string $token + * @param string $password + * + * @throws \app\exceptions\InvalidPasswordHashException + * + * @return \app\dto\ServiceResponseError|\app\dto\ServiceResponseSuccess + */ + public function ResetPassword($token, $password): ServiceResponseSuccess|ServiceResponseError { + $requests = PasswordResetRequest::findByToken($token); + + if (!($requests === null) && count($requests) == 1) { + /** @var PasswordResetRequest $request */ + $request = $requests[0]; + $created = new DateTime($request->created); + $userid = $request->userid; + + $request->delete(); + $created->modify('+2 hours'); + if ($created > new DateTime()) { + $user = $this->getUserById($userid); + + $hashedPassword = PasswordUtil::hash($password); + + if ($hashedPassword === null) { + throw new InvalidPasswordHashException(); + } + + $user->password = $hashedPassword; + + $user->save(); + + return new ServiceResponseSuccess(); + } + } else { + $users = User::findByToken($token); + + if (!($users === null) && count($users) == 1) { + $user = $users[0]; + + $hashedPassword = PasswordUtil::hash($password); + + if ($hashedPassword === null) { + throw new InvalidPasswordHashException(); + } + + $user->token = null; + + $user->password = $hashedPassword; + $user->save(); + + return new ServiceResponseSuccess(); + } + } + + return new ServiceResponseErrorInvalidToken(); + } + + /** + * @permission grants + * + * @param int $userid + * @param string $privilege + * + * @throws \vhs\domain\exceptions\DomainException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return bool + */ + public function RevokePrivilege($userid, $privilege): bool { + if (!CurrentUser::canGrantAllPermissions($privilege)) { + throw new UnauthorizedException('Current user is not allowed to grant this privilege.'); + } + + /** @var User $user */ + $user = $this->getUserById($userid); + + if ($user === null) { + throw new DomainException('User not found'); + } + + /** @var Privilege $priv */ + $priv = Privilege::findByCode($privilege); + + if ($priv === null) { + throw new DomainException('Privilege not found'); + } + + $remove = null; + + // TODO fix typing + /** @disregard P1006 override */ + foreach ($user->privileges->all() as $p) { + if ($p->code == $priv->code) { + $remove = $p; + } + } + + if ($remove === null) { + throw new DomainException('Privilege instance not found'); + } + + // TODO fix typing + /** @disregard P1006 override */ + $user->privileges->remove($remove); + + return $user->save(); + } + + /** + * @permission administrator + * + * @param int $userid + * @param bool|string $cash + * + * @return bool + */ + public function UpdateCash($userid, $cash): bool { + $user = $this->getUserById($userid); + + $user->cash = boolval($cash); + + return $user->save(); + } + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $email + * + * @return bool + */ + public function UpdateEmail($userid, $email): bool { + $user = $this->getUserById($userid); + + if (CurrentUser::hasAnyPermissions('full-profile', 'administrator') !== true) { + $this->throwNotFound(); + } + + $user->email = $email; + + return $user->save(); + } + + /** + * @permission administrator + * + * @param int $userid + * @param string $date + * + * @return bool + */ + public function UpdateExpiry($userid, $date): bool { + $user = $this->getUserById($userid); + + $user->mem_expire = (new DateTime($date))->format('Y-m-d H:i:s'); + + return $user->save(); + } + + /** + * @permission administrator + * + * @param int $userid + * @param mixed $membershipid + * + * @throws \app\exceptions\InvalidInputException + * + * @return bool + */ + public function UpdateMembership($userid, $membershipid): bool { + $user = $this->getUserById($userid); + + /** @var Membership|null */ + $membership = Membership::find($membershipid); + + if ($membership === null) { + throw new InvalidInputException('Invalid user or membership type'); + } + + $user->membership = $membership; + + return $user->save(); + } + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $fname + * @param string $lname + * + * @return bool + */ + public function UpdateName($userid, $fname, $lname): bool { + $user = $this->getUserById($userid); + + if (CurrentUser::hasAnyPermissions('full-profile', 'administrator') !== true) { + $this->throwNotFound(); + } + + $user->fname = $fname; + $user->lname = $lname; + + return $user->save(); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param bool $subscribe + * + * @return bool + */ + public function UpdateNewsletter($userid, $subscribe): bool { + $user = $this->getUserById($userid); + + $user->newsletter = $subscribe ? true : false; + + return $user->save(); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param string $password + * + * @throws \app\exceptions\InvalidPasswordHashException + * + * @return bool + */ + public function UpdatePassword($userid, $password): bool { + $user = $this->getUserById($userid); + + $hashedPassword = PasswordUtil::hash($password); + + if ($hashedPassword === null) { + throw new InvalidPasswordHashException(); + } + + $user->password = $hashedPassword; + + return $user->save(); + } + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $email + * + * @return bool + */ + public function UpdatePaymentEmail($userid, $email): bool { + $user = $this->getUserById($userid); + + if (CurrentUser::hasAnyPermissions('full-profile', 'administrator') !== true) { + $this->throwNotFound(); + } + + $user->payment_email = $email; + + return $user->save(); + } + + /** + * @permission administrator + * + * @param int $userid + * @param string $status + * + * @return bool + */ + public function UpdateStatus($userid, $status): bool { + $user = $this->getUserById($userid); + + if (CurrentUser::hasAnyPermissions('administrator') !== true) { + $this->throwNotFound(); + } + + switch ($status) { + case 'active': + case UserActiveEnum::ACTIVE->value: + case 'true': + $status = UserActiveEnum::ACTIVE->value; + + break; + case 'pending': + case UserActiveEnum::PENDING->value: + $status = UserActiveEnum::PENDING->value; + + break; + case 'banned': + case UserActiveEnum::BANNED->value: + $status = UserActiveEnum::BANNED->value; + + break; + default: + $status = UserActiveEnum::INACTIVE->value; + + break; + } + + $user->active = $status; + + return $user->save(); + } + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $email + * + * @return bool + */ + public function UpdateStripeEmail($userid, $email): bool { + $user = $this->getUserById($userid); + + if (CurrentUser::hasAnyPermissions('full-profile', 'administrator') !== true) { + $this->throwNotFound(); + } + + $user->stripe_email = $email; + + return $user->save(); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param string $username + * + * @return bool + */ + public function UpdateUsername($userid, $username): bool { + $user = $this->getUserById($userid); + + $user->username = $username; + + return $user->save(); + } + + /** + * Summary of AllowedColumns. + * + * @return string[]|null + */ + protected function AllowedColumns(): array|null { + if (CurrentUser::hasAnyPermissions('grants') && !CurrentUser::hasAnyPermissions('administrator')) { + return ['id', 'username', 'fname', 'lname', 'email']; + } else { + return null; + } + } + + /** + * getUserById. + * + * @param mixed $id + * + * @return \app\domain\User + */ + private function getUserById($id): User { + $result = User::find($id); + + if ($result === null) { + $this->throwNotFound(); + } + + return $result; + } +} diff --git a/packages/backend-php/app/handlers/v2/WebHookServiceHandler2.php b/packages/backend-php/app/handlers/v2/WebHookServiceHandler2.php new file mode 100644 index 000000000..a947d5476 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/WebHookServiceHandler2.php @@ -0,0 +1,303 @@ +addUserIDToFilters($userid, $filters); + + return WebHook::count($filters); + } + + /** + * @permission user + * + * @param string $name + * @param string $description + * @param bool $enabled + * @param string $url + * @param string $translation + * @param string $headers + * @param string $method + * @param int $eventid + * + + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\WebHook + */ + public function CreateHook($name, $description, $enabled, $url, $translation, $headers, $method, $eventid): WebHook { + $event = (new EventServiceHandler2($this->context))->GetEvent($eventid); + + $codes = []; + foreach ($event->privileges->all() as $priv) { + array_push($codes, $priv->code); + } + + if (!CurrentUser::hasAllPermissions('administrator') && (count($codes) == 0 || !CurrentUser::hasAllPermissions(...$codes))) { + throw new UnauthorizedException('Insufficient privileges to subscribe to event'); + } + + $hook = new WebHook(); + + $hook->name = $name; + $hook->description = $description; + $hook->enabled = $enabled; + $hook->url = $url; + $hook->translation = $translation; + $hook->headers = $headers; + $hook->method = $method; + $hook->event = $event; + $hook->userid = CurrentUser::getIdentity(); + + $hook->save(); + + return $hook; + } + + /** + * @permission administrator|user + * + * @param int $id + * + * @return void + */ + public function DeleteHook($id): void { + $hook = $this->getWebHookById($id); + + $hook->delete(); + } + + /** + * @permission administrator|user + * + * @param int $id + * @param bool $enabled + * + * @return bool + */ + public function EnableHook($id, $enabled): bool { + $hook = $this->getWebHookById($id); + + $hook->enabled = $enabled; + + return $hook->save(); + } + + /** + * @permission webhook|administrator + * + * @return \app\domain\WebHook[] + */ + public function GetAllHooks(): array { + return WebHook::findAll(); + } + + /** + * @permission user|administrator + * + * @param int $id + * + * @return \app\domain\WebHook|null + */ + public function GetHook($id): WebHook|null { + return $this->getWebHookById($id); + } + + /** + * @permission webhook|administrator + * + * @param string $domain + * @param string $event + * + * @return \app\domain\WebHook[] + */ + public function GetHooks($domain, $event): array { + return WebHook::findByDomainEvent($domain, $event); + } + + /** + * @permission administrator|webhook + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\WebHook[] + */ + public function ListHooks($page, $size, $columns, $order, $filters): array { + return WebHook::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\WebHook[] + */ + public function ListUserHooks($userid, $page, $size, $columns, $order, $filters): array { + $filters = $this->addUserIDToFilters($userid, $filters); + + $cols = explode(',', $columns); + + array_push($cols, 'userid'); + + $columns = implode(',', array_unique($cols)); + + return WebHook::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param int $id + * @param string|string[] $privileges + * + * @return bool + */ + public function PutHookPrivileges($id, $privileges): bool { + $hook = $this->getWebHookById($id); + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + foreach ($hook->privileges->all() as $priv) { + $hook->privileges->remove($priv); + } + + foreach ($privs as $priv) { + if (CurrentUser::hasAnyPermissions('administrator') || CurrentUser::hasAnyPermissions($priv->code)) { + $hook->privileges->add($priv); + } + } + + return $hook->save(); + } + + /** + * @permission administrator|user + * + * @param int $id + * @param string $name + * @param string $description + * @param bool $enabled + * @param string $url + * @param string $translation + * @param string $headers + * @param string $method + * @param int $eventid + * + * @return bool + */ + public function UpdateHook($id, $name, $description, $enabled, $url, $translation, $headers, $method, $eventid): bool { + $hook = $this->getWebHookById($id); + + $event = (new EventServiceHandler2($this->context))->GetEvent($eventid); + + $hook->name = $name; + $hook->description = $description; + $hook->enabled = $enabled; + $hook->url = $url; + $hook->translation = $translation; + $hook->headers = $headers; + $hook->method = $method; + $hook->event = $event; + + return $hook->save(); + } + + /** + * addUserIDToFilters. + * + * @param mixed $userid + * @param string|\vhs\domain\Filter|null $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \vhs\domain\Filter + */ + private function addUserIDToFilters($userid, $filters): Filter { + $userService2 = new UserServiceHandler2(); + + $user = $userService2->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::Equal('userid', $user->id); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + return $filters; + } + + /** + * getWebHookById. + * + * @param mixed $id + * + * @return \app\domain\WebHook + */ + private function getWebHookById($id): WebHook { + /** @var \app\domain\WebHook|null $pref */ + $pref = WebHook::find($id); + + if (is_null($pref)) { + $this->throwNotFound(); + } + + return $pref; + } +} diff --git a/app/include.php b/packages/backend-php/app/include.php similarity index 54% rename from app/include.php rename to packages/backend-php/app/include.php index f885c77e7..103857830 100644 --- a/app/include.php +++ b/packages/backend-php/app/include.php @@ -12,7 +12,8 @@ define('ROOT_NAMESPACE_PATH', dirname(dirname(__FILE__))); -$sqlLog = DEBUG ? new \vhs\loggers\FileLogger(dirname(__FILE__) . '/../logs/sql.log') : new \vhs\loggers\SilentLogger(); +// @phpstan-ignore ternary.alwaysFalse +$sqlLog = DEBUG ? new \vhs\loggers\FileLogger(\vhs\BasePath::getBasePath(false) . '/logs/sql.log') : new \vhs\loggers\SilentLogger(); \vhs\database\Database::setLogger($sqlLog); \vhs\database\Database::setRethrow(true); @@ -26,22 +27,11 @@ \vhs\database\Database::setEngine($mySqlEngine); -$rabbitLog = DEBUG ? new \vhs\loggers\FileLogger(dirname(__FILE__) . '/../logs/rabbit.log') : new \vhs\loggers\SilentLogger(); - -\vhs\messaging\MessageQueue::setLogger($rabbitLog); -\vhs\messaging\MessageQueue::setRethrow(true); - -$rabbitMQ = new \vhs\messaging\engines\RabbitMQ\RabbitMQEngine( - new \vhs\messaging\engines\RabbitMQ\RabbitMQConnectionInfo(RABBITMQ_HOST, RABBITMQ_PORT, RABBITMQ_USER, RABBITMQ_PASSWORD, RABBITMQ_VHOST) -); - -$rabbitMQ->setLogger($rabbitLog); - -\vhs\messaging\MessageQueue::setEngine($rabbitMQ); - \vhs\SplClassLoader::getInstance()->add(new \vhs\SplClassLoaderItem('app', ROOT_NAMESPACE_PATH)); -$serviceLog = DEBUG ? new \vhs\loggers\FileLogger(dirname(__FILE__) . '/service.log') : new \vhs\loggers\SilentLogger(); +// @phpstan-ignore ternary.alwaysFalse +$serviceLog = DEBUG ? new \vhs\loggers\FileLogger(\vhs\BasePath::getBasePath(false) . '/logs/service.log') : new \vhs\loggers\SilentLogger(); -\vhs\services\ServiceRegistry::register($serviceLog, 'web', 'app\\endpoints\\web', ROOT_NAMESPACE_PATH); \vhs\services\ServiceRegistry::register($serviceLog, 'native', 'app\\endpoints\\native', ROOT_NAMESPACE_PATH); +\vhs\services\ServiceRegistry::register($serviceLog, 'v2', 'app\\endpoints\\v2', ROOT_NAMESPACE_PATH); +\vhs\services\ServiceRegistry::register($serviceLog, 'web', 'app\\endpoints\\web', ROOT_NAMESPACE_PATH); diff --git a/app/modules/HttpPaymentGatewayHandler.php b/packages/backend-php/app/modules/HttpPaymentGatewayHandler.php similarity index 80% rename from app/modules/HttpPaymentGatewayHandler.php rename to packages/backend-php/app/modules/HttpPaymentGatewayHandler.php index a947d648b..2ff615f1d 100644 --- a/app/modules/HttpPaymentGatewayHandler.php +++ b/packages/backend-php/app/modules/HttpPaymentGatewayHandler.php @@ -13,6 +13,7 @@ use vhs\web\HttpRequestHandler; use vhs\web\HttpServer; +/** @typescript */ class HttpPaymentGatewayHandler extends HttpRequestHandler { /** @var IPaymentGateway */ private $gateway; @@ -21,7 +22,14 @@ public function __construct(IPaymentGateway $gateway) { $this->gateway = $gateway; } - public function handle(HttpServer $server) { + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function handle(HttpServer $server): void { $server->clear(); $server->code(200); diff --git a/app/modules/HttpPaymentGatewayHandlerModule.php b/packages/backend-php/app/modules/HttpPaymentGatewayHandlerModule.php similarity index 80% rename from app/modules/HttpPaymentGatewayHandlerModule.php rename to packages/backend-php/app/modules/HttpPaymentGatewayHandlerModule.php index c90f46791..c337efcbf 100644 --- a/app/modules/HttpPaymentGatewayHandlerModule.php +++ b/packages/backend-php/app/modules/HttpPaymentGatewayHandlerModule.php @@ -12,6 +12,7 @@ use app\gateways\IPaymentGateway; use vhs\web\modules\HttpRequestHandlerModule; +/** @typescript */ class HttpPaymentGatewayHandlerModule extends HttpRequestHandlerModule { /** * @return HttpPaymentGatewayHandlerModule @@ -28,6 +29,14 @@ final public static function getInstance() { return $aoInstance[$class]; } + /** + * register. + * + * @param \app\gateways\IPaymentGateway $gateway + * @param string|null $url + * + * @return void + */ public static function register(IPaymentGateway $gateway, $url = null) { $path = '/services/gateways/' . $gateway->Name(); if (!is_null($url)) { @@ -42,6 +51,11 @@ public static function register(IPaymentGateway $gateway, $url = null) { self::getInstance()->register_internal('PUT', $path, $handler); } - private function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { } } diff --git a/app/monitors/PaymentMonitor.php b/packages/backend-php/app/monitors/PaymentMonitor.php similarity index 76% rename from app/monitors/PaymentMonitor.php rename to packages/backend-php/app/monitors/PaymentMonitor.php index 840ecb50d..76649ebdc 100644 --- a/app/monitors/PaymentMonitor.php +++ b/packages/backend-php/app/monitors/PaymentMonitor.php @@ -10,25 +10,39 @@ namespace app\monitors; use app\domain\Payment; -use app\domain\User; use app\processors\PaymentProcessor; -use Aws\CloudFront\Exception\Exception; use vhs\Logger; use vhs\monitors\Monitor; +/** @typescript */ class PaymentMonitor extends Monitor { /** @var Logger */ private $logger; + /** @var PaymentProcessor */ private $paymentProcessor; - public function Init(Logger &$logger = null) { + /** + * Init. + * + * @param \vhs\Logger|null $logger + * + * @return void + */ + public function Init(?Logger &$logger = null) { $this->logger = $logger; $this->paymentProcessor = new PaymentProcessor($logger); Payment::onAnyCreated([$this, 'paymentCreated']); } + /** + * paymentCreated. + * + * @param mixed $args + * + * @return void + */ public function paymentCreated($args) { try { $this->paymentProcessor->paymentCreated($args[0]->id); diff --git a/app/monitors/PaypalIpnMonitor.php b/packages/backend-php/app/monitors/PaypalIpnMonitor.php similarity index 90% rename from app/monitors/PaypalIpnMonitor.php rename to packages/backend-php/app/monitors/PaypalIpnMonitor.php index 45047987b..499304c7c 100644 --- a/app/monitors/PaypalIpnMonitor.php +++ b/packages/backend-php/app/monitors/PaypalIpnMonitor.php @@ -15,10 +15,18 @@ use vhs\Logger; use vhs\monitors\Monitor; +/** @typescript */ class PaypalIpnMonitor extends Monitor { /** @var Logger */ private $logger; + /** + * handleCreated. + * + * @param mixed $args + * + * @return void + */ public function handleCreated($args) { /** @var Ipn $ipn */ $ipn = $args[0]; @@ -83,7 +91,14 @@ public function handleCreated($args) { } } - public function Init(Logger &$logger = null) { + /** + * Init. + * + * @param \vhs\Logger $logger + * + * @return void + */ + public function Init(?Logger &$logger = null) { $this->logger = &$logger; Ipn::onAnyCreated([$this, 'handleCreated']); } diff --git a/app/monitors/StripeEventMonitor.php b/packages/backend-php/app/monitors/StripeEventMonitor.php similarity index 91% rename from app/monitors/StripeEventMonitor.php rename to packages/backend-php/app/monitors/StripeEventMonitor.php index 27219db61..20b8961b2 100644 --- a/app/monitors/StripeEventMonitor.php +++ b/packages/backend-php/app/monitors/StripeEventMonitor.php @@ -13,10 +13,18 @@ use vhs\Logger; use vhs\monitors\Monitor; +/** @typescript */ class StripeEventMonitor extends Monitor { /** @var Logger */ private $logger; + /** + * handleCreated. + * + * @param mixed $args + * + * @return void + */ public function handleCreated($args) { $this->logger->log(__METHOD__ . ': ' . json_encode($args, 1)); @@ -72,7 +80,7 @@ public function handleCreated($args) { $item_name = !is_null($line_item->plan->nickname) ? $line_item->plan->nickname : $item_name; } if (isset($line_item->price)) { - $item_amount = !is_null($line_item->price->unit_amount / 100) ? $line_item->price->unit_amount / 100 : $item_amount; + $item_amount = !is_null($line_item->price->unit_amount) ? $line_item->price->unit_amount / 100 : $item_amount; } if (isset($line_item->price)) { $item_number = !is_null($line_item->price->product) ? $line_item->price->product : $item_number; @@ -105,13 +113,24 @@ public function handleCreated($args) { } } - public function Init(Logger &$logger = null) { + /** + * Init. + * + * @param \vhs\Logger|null $logger + * + * @return void + */ + public function Init(?Logger &$logger = null) { $this->logger = &$logger; StripeEvent::onAnyCreated([$this, 'handleCreated']); } /** * Sourced from: https://stackoverflow.com/a/31330346. + * + * @param mixed $name + * + * @return array */ private function split_name($name) { $name = trim($name); diff --git a/app/processors/PaymentProcessor.php b/packages/backend-php/app/processors/PaymentProcessor.php similarity index 83% rename from app/processors/PaymentProcessor.php rename to packages/backend-php/app/processors/PaymentProcessor.php index 41b705f8e..6e3abb4b9 100644 --- a/app/processors/PaymentProcessor.php +++ b/packages/backend-php/app/processors/PaymentProcessor.php @@ -9,29 +9,45 @@ namespace app\processors; +use app\adapters\v2\EmailAdapter2; use app\constants\StringLiterals; use app\domain\Membership; use app\domain\Payment; use app\domain\User; +use app\dto\UserActiveEnum; use app\security\PasswordUtil; -use app\services\EmailService; use app\services\UserService; use DateTime; use vhs\Logger; use vhs\security\CurrentUser; use vhs\security\SystemPrincipal; +/** @typescript */ class PaymentProcessor { - private $emailService; + /** @var mixed */ private $host; - /** @var Logger */ + + /** @var \vhs\Logger|null */ private $logger; - public function __construct(Logger &$logger = null) { + /** + * __construct. + * + * @param \vhs\Logger|null $logger + * + * @return void + */ + public function __construct(?Logger &$logger = null) { $this->logger = $logger; - $this->emailService = new EmailService(); } + /** + * paymentCreated. + * + * @param mixed $id + * + * @return void + */ public function paymentCreated($id) { $suspended_user = CurrentUser::getPrincipal(); CurrentUser::setPrincipal(new SystemPrincipal()); @@ -51,7 +67,7 @@ public function paymentCreated($id) { return; } - /** @var User $user */ + /** @var User|null $user */ $user = null; $users = User::findByPaymentEmail($payment->payer_email); @@ -78,20 +94,34 @@ public function paymentCreated($id) { CurrentUser::setPrincipal($suspended_user); } + /** + * log. + * + * @param mixed $message + * + * @return void + */ private function log($message) { if (!is_null($this->logger)) { $this->logger->log("[PaymentMonitor] {$message}"); } } + /** + * processDonationPayment. + * + * @param \app\domain\User|null $user + * @param \app\domain\Payment $payment + * + * @return void + */ private function processDonationPayment(User $user = null, Payment $payment) { if (is_null($user)) { - $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_error', [ - 'subject' => '[Nomos] Unknown user made a random donation - ' . $payment->payer_fname . ' ' . $payment->lname, - 'message' => - $payment->fname . + EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_error', [ + 'subject' => '[Nomos] Unknown user made a random donation - ' . $payment->payer_fname . ' ' . $payment->payer_lname, + 'message' => $payment->payer_fname . ' ' . - $payment->lname . + $payment->payer_lname . ' with email ' . $payment->payer_email . ' made a donation but we ' . @@ -106,7 +136,7 @@ private function processDonationPayment(User $user = null, Payment $payment) { $payment->currency ]); } else { - $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_donation_random', [ + EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_donation_random', [ 'email' => $payment->payer_email, 'fname' => $payment->payer_fname, 'lname' => $payment->payer_lname, @@ -116,7 +146,7 @@ private function processDonationPayment(User $user = null, Payment $payment) { 'currency' => $payment->currency ]); - $this->emailService->EmailUser($user, 'donation_random', [ + EmailAdapter2::getInstance()->EmailUser($user, 'donation_random', [ 'fname' => $user->fname, 'lname' => $user->lname, 'rate_amount' => $payment->rate_amount, @@ -131,8 +161,16 @@ private function processDonationPayment(User $user = null, Payment $payment) { $payment->save(); } + /** + * processMemberPayment. + * + * @param \app\domain\User|null $user + * @param \app\domain\Payment $payment + * + * @return void + */ private function processMemberPayment(User $user = null, Payment $payment) { - /** @var Membership $membership */ + /** @var Membership|null $membership */ $membership = null; $memberships = Membership::findByCode($payment->item_number); @@ -171,7 +209,7 @@ private function processMemberPayment(User $user = null, Payment $payment) { return; } - $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_newuser', [ + EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_newuser', [ 'email' => $payment->payer_email, 'fname' => $payment->payer_fname, 'lname' => $payment->payer_lname, @@ -182,20 +220,19 @@ private function processMemberPayment(User $user = null, Payment $payment) { if ($user->membership_id != $membership->id) { $userService->UpdateMembership($user->id, $membership->id); } else { - if ($user->active == 'n') { + if ($user->active == UserActiveEnum::INACTIVE->value) { $userService->UpdateStatus($user->id, 'active'); } } $user = User::find($user->id); - if ($user->active != 'y') { - $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_error', [ - 'subject' => "[Nomos] User made payment but isn't active - " . $payment->payer_fname . ' ' . $payment->lname, - 'message' => - $payment->fname . + if ($user->active != UserActiveEnum::ACTIVE->value) { + EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_error', [ + 'subject' => "[Nomos] User made payment but isn't active - " . $payment->payer_fname . ' ' . $payment->payer_lname, + 'message' => $payment->payer_fname . ' ' . - $payment->lname . + $payment->payer_lname . ' with email ' . $payment->payer_email . ' made a payment, but ' . @@ -234,7 +271,7 @@ private function processMemberPayment(User $user = null, Payment $payment) { $payment->status = 1; //processed $payment->save(); - $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_payment', [ + EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_payment', [ 'email' => $payment->payer_email, 'fname' => $payment->payer_fname, 'lname' => $payment->payer_lname, @@ -242,20 +279,27 @@ private function processMemberPayment(User $user = null, Payment $payment) { 'pp' => $payment->pp ]); - $this->emailService->EmailUser($user, 'payment', [ + EmailAdapter2::getInstance()->EmailUser($user, 'payment', [ 'host' => $this->host, 'fname' => $user->fname ]); } + /** + * processMembershipCardPayment. + * + * @param \app\domain\User|null $user + * @param \app\domain\Payment $payment + * + * @return void + */ private function processMembershipCardPayment(User $user = null, Payment $payment) { if (is_null($user)) { - $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_error', [ - 'subject' => '[Nomos] Unknown user purchased Membership Card - ' . $payment->payer_fname . ' ' . $payment->lname, - 'message' => - $payment->fname . + EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_error', [ + 'subject' => '[Nomos] Unknown user purchased Membership Card - ' . $payment->payer_fname . ' ' . $payment->payer_lname, + 'message' => $payment->payer_fname . ' ' . - $payment->lname . + $payment->payer_lname . ' with email ' . $payment->payer_email . ' purchased a membership card but we ' . @@ -263,13 +307,13 @@ private function processMembershipCardPayment(User $user = null, Payment $paymen ' we can issue a member card.' ]); } else { - $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_membercard_purchased', [ + EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_membercard_purchased', [ 'email' => $payment->payer_email, 'fname' => $payment->payer_fname, 'lname' => $payment->payer_lname ]); - $this->emailService->EmailUser($user, 'membercard_purchased', [ + EmailAdapter2::getInstance()->EmailUser($user, 'membercard_purchased', [ 'fname' => $user->fname, 'lname' => $user->lname ]); @@ -282,6 +326,13 @@ private function processMembershipCardPayment(User $user = null, Payment $paymen $payment->save(); } + /** + * resolveLegacyPayments. + * + * @param \app\domain\Payment $payment + * + * @return \app\domain\Payment + */ private function resolveLegacyPayments(Payment $payment) { /* * select distinct item_name, item_number, rate_amount from payments where date > '2016-05-01'; diff --git a/app/resources/Resource.php b/packages/backend-php/app/resources/Resource.php similarity index 76% rename from app/resources/Resource.php rename to packages/backend-php/app/resources/Resource.php index e1369781a..7a95f466b 100644 --- a/app/resources/Resource.php +++ b/packages/backend-php/app/resources/Resource.php @@ -11,7 +11,9 @@ use vhs\Singleton; +/** @typescript */ class Resource extends Singleton { + /** @var array */ private $data; protected function __construct() { @@ -20,6 +22,13 @@ protected function __construct() { //$this->data[] = } + /** + * __get. + * + * @param string $name + * + * @return mixed + */ public function __get($name) { if (array_key_exists($name, $this->data)) { return $this->data[$name]; diff --git a/app/schema/AccessLogSchema.php b/packages/backend-php/app/schema/AccessLogSchema.php similarity index 84% rename from app/schema/AccessLogSchema.php rename to packages/backend-php/app/schema/AccessLogSchema.php index 9a11625d8..dfae92442 100644 --- a/app/schema/AccessLogSchema.php +++ b/packages/backend-php/app/schema/AccessLogSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class AccessLogSchema extends Schema { public static function init() { $table = new Table('accesslog'); @@ -27,8 +28,12 @@ public static function init() { $table->addColumn('time', Type::DateTime(false, date('Y-m-d H:i:s'))); $table->addColumn('userid', Type::Int()); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setAccess(PrivilegedAccess::GenerateAccess('accesslog', $table, $table->columns->userid)); return $table; diff --git a/packages/backend-php/app/schema/AccessTokenSchema.php b/packages/backend-php/app/schema/AccessTokenSchema.php new file mode 100644 index 000000000..b26380323 --- /dev/null +++ b/packages/backend-php/app/schema/AccessTokenSchema.php @@ -0,0 +1,63 @@ +addColumn('id', Type::Int(false, 0)); + $table->addColumn('token', Type::String()); + $table->addColumn('expires', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('userid', Type::Int()); + $table->addColumn('appclientid', Type::Int()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->id), + + // TODO implement proper typing + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->userid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->appclientid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + AppClientSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + AppClientSchema::Columns()->id + ) + ); + + $table->setAccess(PrivilegedAccess::GenerateAccess('accesstoken', $table)); + + return $table; + } +} diff --git a/app/schema/AppClientSchema.php b/packages/backend-php/app/schema/AppClientSchema.php similarity index 63% rename from app/schema/AppClientSchema.php rename to packages/backend-php/app/schema/AppClientSchema.php index 25830e490..e6d766577 100644 --- a/app/schema/AppClientSchema.php +++ b/packages/backend-php/app/schema/AppClientSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class AppClientSchema extends Schema { public static function init() { $table = new Table('appclient'); @@ -30,10 +31,24 @@ public static function init() { $table->addColumn('enabled', Type::Bool(false, false)); $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id) + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->userid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ) ); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setAccess(PrivilegedAccess::GenerateAccess('appclient', $table, $table->columns->userid)); return $table; diff --git a/app/schema/EmailSchema.php b/packages/backend-php/app/schema/EmailSchema.php similarity index 90% rename from app/schema/EmailSchema.php rename to packages/backend-php/app/schema/EmailSchema.php index f29e3957e..fd6a40dfe 100644 --- a/app/schema/EmailSchema.php +++ b/packages/backend-php/app/schema/EmailSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class EmailSchema extends Schema { public static function init() { $table = new Table('email_templates'); @@ -27,6 +28,8 @@ public static function init() { $table->addColumn('body', Type::Text()); $table->addColumn('html', Type::Text()); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); $table->setAccess(PrivilegedAccess::GenerateAccess('emailtemplate', $table)); diff --git a/packages/backend-php/app/schema/EventPrivilegeSchema.php b/packages/backend-php/app/schema/EventPrivilegeSchema.php new file mode 100644 index 000000000..bcbaec619 --- /dev/null +++ b/packages/backend-php/app/schema/EventPrivilegeSchema.php @@ -0,0 +1,60 @@ +addColumn('eventid', Type::Int()); + $table->addColumn('privilegeid', Type::Int()); + $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('notes', Type::Text()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->eventid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->privilegeid), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->eventid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + EventSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + EventSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->privilegeid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + PrivilegeSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + PrivilegeSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/app/schema/EventSchema.php b/packages/backend-php/app/schema/EventSchema.php similarity index 90% rename from app/schema/EventSchema.php rename to packages/backend-php/app/schema/EventSchema.php index ecf066d22..81c27e06a 100644 --- a/app/schema/EventSchema.php +++ b/packages/backend-php/app/schema/EventSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class EventSchema extends Schema { /** * @return Table @@ -29,6 +30,8 @@ public static function init() { $table->addColumn('description', Type::Text()); $table->addColumn('enabled', Type::Bool(false, false)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); $table->setAccess(PrivilegedAccess::GenerateAccess('event', $table)); diff --git a/app/schema/GenuineCardSchema.php b/packages/backend-php/app/schema/GenuineCardSchema.php similarity index 86% rename from app/schema/GenuineCardSchema.php rename to packages/backend-php/app/schema/GenuineCardSchema.php index 1bcc7f1d8..c20839ab3 100644 --- a/app/schema/GenuineCardSchema.php +++ b/packages/backend-php/app/schema/GenuineCardSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class GenuineCardSchema extends Schema { public static function init() { $table = new Table('genuinecard'); @@ -29,8 +30,12 @@ public static function init() { $table->addColumn('owneremail', Type::String(true, '', 255)); $table->addColumn('notes', Type::String(true, '', 255)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setAccess(PrivilegedAccess::GenerateAccess('genuinecard', $table, $table->columns->userid)); return $table; diff --git a/app/schema/IpnSchema.php b/packages/backend-php/app/schema/IpnSchema.php similarity index 92% rename from app/schema/IpnSchema.php rename to packages/backend-php/app/schema/IpnSchema.php index d5e646d2d..a3a19c654 100644 --- a/app/schema/IpnSchema.php +++ b/packages/backend-php/app/schema/IpnSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class IpnSchema extends Schema { public static function init() { $table = new Table('ipnrequest'); @@ -30,6 +31,8 @@ public static function init() { $table->addColumn('item_number', Type::String(false, '', 255)); $table->addColumn('raw', Type::Text()); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); $table->setAccess(PrivilegedAccess::GenerateAccess('ipn', $table)); diff --git a/packages/backend-php/app/schema/KeyPrivilegeSchema.php b/packages/backend-php/app/schema/KeyPrivilegeSchema.php new file mode 100644 index 000000000..e8290a2bf --- /dev/null +++ b/packages/backend-php/app/schema/KeyPrivilegeSchema.php @@ -0,0 +1,62 @@ +addColumn('keyid', Type::Int()); + $table->addColumn('privilegeid', Type::Int()); + $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('notes', Type::Text()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->keyid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->privilegeid), + // TODO implement proper typing + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->keyid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + KeySchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + KeySchema::Columns()->id + ), + // TODO implement proper typing + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->privilegeid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + PrivilegeSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + PrivilegeSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/app/schema/KeySchema.php b/packages/backend-php/app/schema/KeySchema.php similarity index 61% rename from app/schema/KeySchema.php rename to packages/backend-php/app/schema/KeySchema.php index d9189d301..3a1b549e6 100644 --- a/app/schema/KeySchema.php +++ b/packages/backend-php/app/schema/KeySchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class KeySchema extends Schema { public static function init() { $table = new Table('keys'); @@ -28,10 +29,24 @@ public static function init() { $table->addColumn('expires', Type::DateTime()); $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id) + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->userid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ) ); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setAccess(PrivilegedAccess::GenerateAccess('key', $table, $table->columns->userid)); return $table; diff --git a/packages/backend-php/app/schema/MembershipPrivilegeSchema.php b/packages/backend-php/app/schema/MembershipPrivilegeSchema.php new file mode 100644 index 000000000..fbda2153a --- /dev/null +++ b/packages/backend-php/app/schema/MembershipPrivilegeSchema.php @@ -0,0 +1,60 @@ +addColumn('membershipid', Type::Int()); + $table->addColumn('privilegeid', Type::Int()); + $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('notes', Type::Text()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->membershipid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->privilegeid), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->membershipid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + MembershipSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + MembershipSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->privilegeid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + PrivilegeSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + PrivilegeSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/app/schema/MembershipSchema.php b/packages/backend-php/app/schema/MembershipSchema.php similarity index 92% rename from app/schema/MembershipSchema.php rename to packages/backend-php/app/schema/MembershipSchema.php index 963356917..0dafefc11 100644 --- a/app/schema/MembershipSchema.php +++ b/packages/backend-php/app/schema/MembershipSchema.php @@ -15,6 +15,9 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** + * @typescript + */ class MembershipSchema extends Schema { public static function init() { $table = new Table('memberships'); @@ -31,6 +34,8 @@ public static function init() { $table->addColumn('private', Type::Bool(false, false)); $table->addColumn('active', Type::Bool(false, false)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); $table->setAccess(PrivilegedAccess::GenerateAccess('membership', $table)); diff --git a/app/schema/PasswordResetRequestSchema.php b/packages/backend-php/app/schema/PasswordResetRequestSchema.php similarity index 57% rename from app/schema/PasswordResetRequestSchema.php rename to packages/backend-php/app/schema/PasswordResetRequestSchema.php index 1950633d0..55ce319b5 100644 --- a/app/schema/PasswordResetRequestSchema.php +++ b/packages/backend-php/app/schema/PasswordResetRequestSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class PasswordResetRequestSchema extends Schema { public static function init() { $table = new Table('passwordresetrequests'); @@ -25,10 +26,24 @@ public static function init() { $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id) + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->userid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ) ); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setAccess(PrivilegedAccess::GenerateAccess('passwordresetrequest', $table, $table->columns->userid)); return $table; diff --git a/app/schema/PaymentSchema.php b/packages/backend-php/app/schema/PaymentSchema.php similarity index 59% rename from app/schema/PaymentSchema.php rename to packages/backend-php/app/schema/PaymentSchema.php index eefa4d832..4da50f268 100644 --- a/app/schema/PaymentSchema.php +++ b/packages/backend-php/app/schema/PaymentSchema.php @@ -9,14 +9,13 @@ namespace app\schema; -use app\schema\MembershipSchema; -use app\schema\UserSchema; use app\security\PrivilegedAccess; use vhs\database\constraints\Constraint; use vhs\database\Table; use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class PaymentSchema extends Schema { public static function init() { $table = new Table('payments'); @@ -38,11 +37,35 @@ public static function init() { $table->addColumn('item_number', Type::String(true, null, 255)); $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->membership_id, MembershipSchema::Table(), MembershipSchema::Columns()->id), - Constraint::ForeignKey($table->columns->user_id, UserSchema::Table(), UserSchema::Columns()->id) + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->membership_id, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + MembershipSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + MembershipSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->user_id, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ) ); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setAccess(PrivilegedAccess::GenerateAccess('payment', $table, $table->columns->user_id)); return $table; diff --git a/app/schema/PrivilegeSchema.php b/packages/backend-php/app/schema/PrivilegeSchema.php similarity index 90% rename from app/schema/PrivilegeSchema.php rename to packages/backend-php/app/schema/PrivilegeSchema.php index 9424614ae..9a563c43b 100644 --- a/app/schema/PrivilegeSchema.php +++ b/packages/backend-php/app/schema/PrivilegeSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class PrivilegeSchema extends Schema { public static function init() { $table = new Table('privileges'); @@ -26,6 +27,8 @@ public static function init() { $table->addColumn('icon', Type::String(false, '', 255)); $table->addColumn('enabled', Type::Bool(false, false)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); $table->setAccess(PrivilegedAccess::GenerateAccess('privilege', $table)); diff --git a/packages/backend-php/app/schema/RefreshTokenSchema.php b/packages/backend-php/app/schema/RefreshTokenSchema.php new file mode 100644 index 000000000..46b2e184d --- /dev/null +++ b/packages/backend-php/app/schema/RefreshTokenSchema.php @@ -0,0 +1,63 @@ +addColumn('id', Type::Int(false, 0)); + $table->addColumn('token', Type::String()); + $table->addColumn('expires', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('userid', Type::Int()); + $table->addColumn('appclientid', Type::Int()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->id), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->userid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->appclientid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + AppClientSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + AppClientSchema::Columns()->id + ) + ); + + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->setAccess(PrivilegedAccess::GenerateAccess('accesstoken', $table, $table->columns->userid)); + + return $table; + } +} diff --git a/app/schema/SettingsSchema.php b/packages/backend-php/app/schema/SettingsSchema.php similarity index 99% rename from app/schema/SettingsSchema.php rename to packages/backend-php/app/schema/SettingsSchema.php index 094b453e2..2adb545cd 100644 --- a/app/schema/SettingsSchema.php +++ b/packages/backend-php/app/schema/SettingsSchema.php @@ -14,6 +14,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class SettingsSchema extends Schema { /** * @return Table diff --git a/app/schema/StripeEventSchema.php b/packages/backend-php/app/schema/StripeEventSchema.php similarity index 91% rename from app/schema/StripeEventSchema.php rename to packages/backend-php/app/schema/StripeEventSchema.php index 5f49d4219..a95f48c31 100644 --- a/app/schema/StripeEventSchema.php +++ b/packages/backend-php/app/schema/StripeEventSchema.php @@ -13,6 +13,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class StripeEventSchema extends Schema { public static function init() { $table = new Table('stripe_events'); @@ -28,6 +29,8 @@ public static function init() { $table->addColumn('api_version', Type::String(false, '', 255)); $table->addColumn('raw', Type::Text()); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); $table->setAccess(PrivilegedAccess::GenerateAccess('stripe_events', $table)); diff --git a/packages/backend-php/app/schema/SystemPreferencePrivilegeSchema.php b/packages/backend-php/app/schema/SystemPreferencePrivilegeSchema.php new file mode 100644 index 000000000..7c5dc4921 --- /dev/null +++ b/packages/backend-php/app/schema/SystemPreferencePrivilegeSchema.php @@ -0,0 +1,60 @@ +addColumn('systempreferenceid', Type::Int()); + $table->addColumn('privilegeid', Type::Int()); + $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('notes', Type::Text()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->systempreferenceid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->privilegeid), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->systempreferenceid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + SystemPreferenceSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + SystemPreferenceSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->privilegeid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + PrivilegeSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + PrivilegeSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/app/schema/SystemPreferenceSchema.php b/packages/backend-php/app/schema/SystemPreferenceSchema.php similarity index 89% rename from app/schema/SystemPreferenceSchema.php rename to packages/backend-php/app/schema/SystemPreferenceSchema.php index cf3cc66d6..9e2b2ea54 100644 --- a/app/schema/SystemPreferenceSchema.php +++ b/packages/backend-php/app/schema/SystemPreferenceSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class SystemPreferenceSchema extends Schema { public static function init() { $table = new Table('systempreferences'); @@ -25,6 +26,8 @@ public static function init() { $table->addColumn('enabled', Type::Bool(false, true)); $table->addColumn('notes', Type::Text()); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); $table->setAccess(PrivilegedAccess::GenerateAccess('systempreference', $table)); diff --git a/packages/backend-php/app/schema/UserPrivilegeSchema.php b/packages/backend-php/app/schema/UserPrivilegeSchema.php new file mode 100644 index 000000000..4caa9682a --- /dev/null +++ b/packages/backend-php/app/schema/UserPrivilegeSchema.php @@ -0,0 +1,60 @@ +addColumn('userid', Type::Int()); + $table->addColumn('privilegeid', Type::Int()); + $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('notes', Type::Text()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->userid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->privilegeid), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->userid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->privilegeid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + PrivilegeSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + PrivilegeSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/app/schema/UserSchema.php b/packages/backend-php/app/schema/UserSchema.php similarity index 70% rename from app/schema/UserSchema.php rename to packages/backend-php/app/schema/UserSchema.php index aa919d213..ee656d0cb 100644 --- a/app/schema/UserSchema.php +++ b/packages/backend-php/app/schema/UserSchema.php @@ -9,12 +9,14 @@ namespace app\schema; +use app\dto\UserActiveEnum; use app\security\PrivilegedAccess; use vhs\database\constraints\Constraint; use vhs\database\Table; use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class UserSchema extends Schema { public static function init() { $table = new Table('users'); @@ -38,17 +40,34 @@ public static function init() { $table->addColumn('lastlogin', Type::DateTime(true, date('Y-m-d H:i:s'))); $table->addColumn('lastip', Type::String(true, '0', 16)); $table->addColumn('avatar', Type::String(true, '0', 150)); - $table->addColumn('active', Type::Enum('n', 'y', 't', 'b')); + $table->addColumn( + 'active', + Type::Enum(UserActiveEnum::INACTIVE->value, UserActiveEnum::ACTIVE->value, UserActiveEnum::PENDING->value, UserActiveEnum::BANNED->value) + ); $table->addColumn('paypal_id', Type::String(false, '', 255)); $table->addColumn('payment_email', Type::String(false, '', 255)); $table->addColumn('stripe_id', Type::String(false, '', 255)); $table->addColumn('stripe_email', Type::String(false, '', 255)); $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->membership_id, MembershipSchema::Table(), MembershipSchema::Columns()->id) + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->membership_id, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + MembershipSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + MembershipSchema::Columns()->id + ) ); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setAccess(PrivilegedAccess::GenerateAccess('user', $table, $table->columns->id)); return $table; diff --git a/packages/backend-php/app/schema/WebHookPrivilegeSchema.php b/packages/backend-php/app/schema/WebHookPrivilegeSchema.php new file mode 100644 index 000000000..da6aea6cc --- /dev/null +++ b/packages/backend-php/app/schema/WebHookPrivilegeSchema.php @@ -0,0 +1,60 @@ +addColumn('webhookid', Type::Int()); + $table->addColumn('privilegeid', Type::Int()); + $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('notes', Type::Text()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->webhookid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->privilegeid), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->webhookid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + WebHookSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + WebHookSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->privilegeid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + PrivilegeSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + PrivilegeSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/app/schema/WebHookSchema.php b/packages/backend-php/app/schema/WebHookSchema.php similarity index 52% rename from app/schema/WebHookSchema.php rename to packages/backend-php/app/schema/WebHookSchema.php index 2d46bdb88..c3622d88d 100644 --- a/app/schema/WebHookSchema.php +++ b/packages/backend-php/app/schema/WebHookSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class WebHookSchema extends Schema { /** * @return Table @@ -34,11 +35,35 @@ public static function init() { $table->addColumn('eventid', Type::Int()); $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id), - Constraint::ForeignKey($table->columns->eventid, EventSchema::Table(), EventSchema::Columns()->id) + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->userid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->eventid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + EventSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + EventSchema::Columns()->id + ) ); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setAccess(PrivilegedAccess::GenerateAccess('webhook', $table, $table->columns->userid)); return $table; diff --git a/app/security/Authenticate.php b/packages/backend-php/app/security/Authenticate.php similarity index 69% rename from app/security/Authenticate.php rename to packages/backend-php/app/security/Authenticate.php index a59de1c91..925291e8b 100644 --- a/app/security/Authenticate.php +++ b/packages/backend-php/app/security/Authenticate.php @@ -13,6 +13,9 @@ use app\domain\AccessToken; use app\domain\Key; use app\domain\User; +use app\dto\UserActiveEnum; +use app\exceptions\InvalidAccessTokenCredentialsException; +use app\exceptions\InvalidKeyCredentialsException; use app\security\credentials\ApiCredentials; use app\security\credentials\PinCredentials; use app\security\credentials\RfidCredentials; @@ -29,7 +32,20 @@ use vhs\security\UserPassCredentials; use vhs\Singleton; +/** + * @method static \app\security\Authenticate getInstance() + * + * @typescript + */ class Authenticate extends Singleton implements IAuthenticate { + /** + * authenticateOnly. + * + * @param mixed $username + * @param mixed $password + * + * @return \app\domain\User + */ public static function authenticateOnly($username, $password) { return self::userLogin($username, $password, true); } @@ -48,37 +64,73 @@ public static function isAuthenticated() { return !CurrentUser::isAnon(); } + /** + * login. + * + * @param ICredentials $credentials + * + * @throws \app\exceptions\InvalidAccessTokenCredentialsException + * @throws \app\exceptions\InvalidInputException + * @throws \app\exceptions\InvalidKeyCredentialsException + * @throws \app\exceptions\InvalidPasswordHashException + * @throws \app\exceptions\UserAlreadyExistsException + * @throws \vhs\domain\exceptions\DomainException + * @throws \vhs\exceptions\HttpException + * @throws \vhs\security\exceptions\InvalidCredentials + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return void + */ public static function login(ICredentials $credentials) { switch (get_class($credentials)) { case 'vhs\\security\\UserPassCredentials': /** @var UserPassCredentials $credentials */ self::userLogin($credentials->getUsername(), $credentials->getPassword()); + break; case 'app\\security\\credentials\\ApiCredentials': /** @var ApiCredentials $credentials */ self::keyLogin(Key::findByApiKey($credentials->getToken()), $credentials); + break; case 'app\\security\\credentials\\RfidCredentials': /** @var RfidCredentials $credentials */ self::keyLogin(Key::findByRfid($credentials->getToken()), $credentials); + break; case 'app\\security\\credentials\\PinCredentials': /** @var PinCredentials $credentials */ self::keyLogin(Key::findByPin($credentials->getToken()), $credentials); + break; case 'vhs\\security\\BearerTokenCredentials': /** @var BearerTokenCredentials $credentials */ self::bearerLogin($credentials); + break; default: throw new InvalidCredentials('"Unsupported authentication type."'); } } + /** + * logout. + * + * @return void + */ public static function logout() { CurrentUser::setPrincipal(new AnonPrincipal()); } + /** + * bearerLogin. + * + * @param BearerTokenCredentials $credentials + * + * @throws \app\exceptions\InvalidAccessTokenCredentialsException + * + * @return void + */ private static function bearerLogin(BearerTokenCredentials $credentials) { $ipaddr = self::getRemoteIP(); @@ -92,7 +144,8 @@ private static function bearerLogin(BearerTokenCredentials $credentials) { if (is_null($token) || is_null($token->user)) { AccessLog::log($credentials->getToken(), 'bearer', false, $ipaddr); - throw new InvalidCredentials(message: '"Invalid access token"'); + + throw new InvalidAccessTokenCredentialsException(); } if ( @@ -107,12 +160,13 @@ private static function bearerLogin(BearerTokenCredentials $credentials) { AccessLog::log($credentials->getToken(), 'bearer', true, $ipaddr, $token->user->id); } else { AccessLog::log($credentials->getToken(), 'bearer', false, $ipaddr, $token->user->id); - throw new InvalidCredentials('"Invalid access token"'); + + throw new InvalidAccessTokenCredentialsException(); } } /** - * @param $user + * @param \app\domain\User $user * * @return UserPrincipal */ @@ -130,9 +184,14 @@ private static function buildPrincipal($user) { $privileges = array_merge( $membershipPrivs, - array_map(function ($privilege) { - return $privilege->code; - }, $user->privileges->all()) + array_map( + function ($privilege) { + return $privilege->code; + }, + // TODO fix typing + /** @disregard P1006 override */ + $user->privileges->all() + ) ); foreach ($privileges as $priv) { @@ -152,11 +211,11 @@ private static function buildPrincipal($user) { } /** - * @param $username + * @param string $username * - * @return User + * @throws \vhs\security\exceptions\InvalidCredentials * - * @throws InvalidCredentials + * @return User */ private static function findUser($username) { $users = User::findByUsername($username); @@ -173,40 +232,62 @@ private static function findUser($username) { return $users[0]; } + /** + * getRemoteIP. + * + * @return string|null + */ private static function getRemoteIP() { $ipaddr = null; - if (isset($_SERVER) && array_key_exists('REMOTE_ADDR', $_SERVER)) { + + if (array_key_exists('REMOTE_ADDR', $_SERVER)) { $ipaddr = $_SERVER['REMOTE_ADDR']; } return $ipaddr; } - private static function isUserValid($user) { + /** + * isUserValid. + * + * @param \app\domain\User $user + * + * @throws \vhs\security\exceptions\InvalidCredentials + * + * @return bool + */ + private static function isUserValid(User $user) { switch ($user->active) { - case 'n': //not active + case UserActiveEnum::INACTIVE->value: //not active throw new InvalidCredentials('"Your account is not activated"'); - break; - case 'y': //yes they are active + case UserActiveEnum::ACTIVE->value: //yes they are active return true; - break; - case 't': //pending email verification + case UserActiveEnum::PENDING->value: //pending email verification throw new InvalidCredentials('"You need to verify your email address"'); - break; - case 'b': //banned + case UserActiveEnum::BANNED->value: //banned throw new InvalidCredentials('"Your account has been banned"'); - break; + default: + return false; } - - return false; } + /** + * keyLogin. + * + * @param mixed $keys + * @param \app\security\credentials\TokenCredentials $credentials + * + * @throws \app\exceptions\InvalidKeyCredentialsException + * + * @return void + */ private static function keyLogin($keys, TokenCredentials $credentials) { $ipaddr = self::getRemoteIP(); if (count($keys) != 1) { AccessLog::log($credentials->getToken(), $credentials->getType(), false, $ipaddr); - throw new InvalidCredentials('"Invalid key"'); + + throw new InvalidKeyCredentialsException(); } $key = $keys[0]; @@ -222,7 +303,8 @@ private static function keyLogin($keys, TokenCredentials $credentials) { $user = User::find($key->userid); } catch (\Exception $ex) { AccessLog::log($credentials->getToken(), $credentials->getType(), false, $ipaddr, $key->userid); - throw new InvalidCredentials('"Invalid key"'); + + throw new InvalidKeyCredentialsException(); } if (!is_null($user) && self::isUserValid($user)) { @@ -236,18 +318,25 @@ private static function keyLogin($keys, TokenCredentials $credentials) { array_map(function ($privilege) { return $privilege->code; }, $user->membership->privileges->all()), - array_map(function ($privilege) { - return $privilege->code; - }, $user->privileges->all()) + array_map( + function ($privilege) { + return $privilege->code; + }, + // TODO fix typing + /** @disregard P1006 override */ + $user->privileges->all() + ) ); } } else { AccessLog::log($credentials->getToken(), $credentials->getType(), false, $ipaddr); - throw new InvalidCredentials('"Invalid key"'); + + throw new InvalidKeyCredentialsException(); } } $grants = []; + foreach ($privileges as $priv) { if (strpos($priv, 'grant:') === 0) { array_push($grants, substr($priv, 6)); @@ -258,16 +347,18 @@ private static function keyLogin($keys, TokenCredentials $credentials) { array_push($privileges, 'grants'); } - CurrentUser::setPrincipal(new TokenPrincipal($identity, $privileges, $grants, $name)); + CurrentUser::setPrincipal(new TokenPrincipal(id: $identity, permissions: $privileges, grants: $grants, name: $name)); AccessLog::log($credentials->getToken(), $credentials->getType(), true, $ipaddr, $key->userid); } /** - * @param $user - * @param $ipaddr + * record login. + * + * @param \app\domain\User $user + * @param string $ipaddr * - * @throws \Exception + * @return void */ private static function recordLogin($user, $ipaddr) { $user->lastlogin = date(Database::DateFormat()); @@ -282,6 +373,18 @@ private static function recordLogin($user, $ipaddr) { } } + /** + * userLogin. + * + * @param string $username + * @param string $password + * @param bool $authonly + * + + * @throws \vhs\security\exceptions\InvalidCredentials + * + * @return \app\domain\User + */ private static function userLogin($username, $password, $authonly = false) { $ipaddr = self::getRemoteIP(); @@ -289,6 +392,7 @@ private static function userLogin($username, $password, $authonly = false) { $user = self::findUser($username); } catch (\Exception $ex) { AccessLog::log($username, 'userpass', false, $ipaddr); + throw $ex; } @@ -304,6 +408,7 @@ private static function userLogin($username, $password, $authonly = false) { return $user; } else { AccessLog::log($username, 'userpass', false, $ipaddr, $user->id); + throw new InvalidCredentials('"Incorrect username or password"'); } } diff --git a/app/security/ColumnPrivilegedAccess.php b/packages/backend-php/app/security/ColumnPrivilegedAccess.php similarity index 60% rename from app/security/ColumnPrivilegedAccess.php rename to packages/backend-php/app/security/ColumnPrivilegedAccess.php index c0401f78f..2bb710e05 100644 --- a/app/security/ColumnPrivilegedAccess.php +++ b/packages/backend-php/app/security/ColumnPrivilegedAccess.php @@ -12,26 +12,60 @@ use vhs\database\Column; use vhs\database\Table; +/** @typescript */ class ColumnPrivilegedAccess extends PrivilegedAccess { /** @var Column */ private $column; + /** @var string[] */ - private $privileges; + private $privileges = []; + /** + * __construct. + * + * @param \vhs\database\Column|null $ownerColumn + * @param \vhs\database\Column $column + * @param string ...$privileges + * + * @return void + */ public function __construct(Column $ownerColumn = null, Column $column, ...$privileges) { parent::__construct($ownerColumn); $this->column = $column; $this->privileges = $privileges; } + /** + * CanRead. + * + * @param mixed $record + * @param \vhs\database\Table $table + * @param \vhs\database\Column $column + * + * @return bool + */ public function CanRead($record, Table $table, Column $column) { return $column === $this->column && $this->hasPrivilegedAccess($record, ...$this->privileges); } + /** + * CanWrite. + * + * @param mixed $record + * @param \vhs\database\Table $table + * @param \vhs\database\Column $column + * + * @return bool + */ public function CanWrite($record, Table $table, Column $column) { return $column === $this->column && $this->hasPrivilegedAccess($record, ...$this->privileges); } + /** + * jsonSerialize. + * + * @return mixed + */ public function jsonSerialize(): mixed { return [ 'type' => 'column', @@ -45,7 +79,21 @@ public function jsonSerialize(): mixed { ]; } - public function serialize(): mixed { + /** + * serialize. + * + * @return string + */ + public function serialize(): string { + return json_encode($this->__serialize()); + } + + /** + * __serialize. + * + * @return array + */ + public function __serialize(): array { return [ 'type' => 'column', 'column' => [ @@ -58,10 +106,13 @@ public function serialize(): mixed { ]; } - public function __serialize() { - return $this->serialize(); - } - + /** + * __unserialize. + * + * @param mixed $data + * + * @return void + */ public function __unserialize($data): void { // TODO maybe implement? } diff --git a/app/security/HttpApiAuthModule.php b/packages/backend-php/app/security/HttpApiAuthModule.php similarity index 71% rename from app/security/HttpApiAuthModule.php rename to packages/backend-php/app/security/HttpApiAuthModule.php index 9acdc5b50..7c7d259a0 100644 --- a/app/security/HttpApiAuthModule.php +++ b/packages/backend-php/app/security/HttpApiAuthModule.php @@ -15,19 +15,42 @@ use vhs\web\HttpServer; use vhs\web\IHttpModule; +/** @typescript */ class HttpApiAuthModule implements IHttpModule { + /** @var mixed */ private $authorizer; + /** + * __construct. + * + * @param \vhs\security\IAuthenticate $authorizer + */ public function __construct(IAuthenticate $authorizer) { $this->authorizer = $authorizer; } + /** + * endResponse. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function endResponse(HttpServer $server) { if (array_key_exists('X-Api-Key', $server->request->headers) && $this->authorizer->isAuthenticated()) { $this->authorizer->logout(); } } + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return void + */ public function handle(HttpServer $server) { if (array_key_exists('X-Api-Key', $server->request->headers) && !$this->authorizer->isAuthenticated()) { try { @@ -38,6 +61,14 @@ public function handle(HttpServer $server) { } } + /** + * handleException. + * + * @param \vhs\web\HttpServer $server + * @param \Exception $ex + * + * @return void + */ public function handleException(HttpServer $server, \Exception $ex) { if (get_class($ex) === 'vhs\\security\\exceptions\\UnauthorizedException') { $server->clear(); diff --git a/app/security/PasswordUtil.php b/packages/backend-php/app/security/PasswordUtil.php similarity index 67% rename from app/security/PasswordUtil.php rename to packages/backend-php/app/security/PasswordUtil.php index 1f6e6a19b..b1ac88199 100644 --- a/app/security/PasswordUtil.php +++ b/packages/backend-php/app/security/PasswordUtil.php @@ -17,7 +17,16 @@ define('PASSWORD_DEFAULT', PASSWORD_BCRYPT); } +/** @typescript */ class PasswordUtil { + /** + * check. + * + * @param string $password + * @param string $hash + * + * @return bool + */ public static function check($password, $hash) { return password_verify(sha1($password), $hash); // return self::password_verify(sha1($password), $hash); @@ -26,22 +35,41 @@ public static function check($password, $hash) { /** * check if a variable is a valid string. * - * @param any $testVal + * @param mixed $testVal * - * @return boolean + * @return bool */ public static function checkValidString($testVal) { - gettype($testVal) === 'string' && $testVal !== ''; + return gettype($testVal) === 'string' && $testVal !== ''; } + /** + * generate. + * + * @return string + */ public static function generate() { return self::hash(self::generateRandomString()); } + /** + * hash. + * + * @param mixed $password + * + * @return string + */ public static function hash($password) { return password_hash(sha1($password), PASSWORD_BCRYPT); } + /** + * generateRandomString. + * + * @param int $length + * + * @return string + */ private static function generateRandomString($length = 16) { $randStr = str_shuffle(str_repeat('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', $length)); diff --git a/app/security/PrivilegedAccess.php b/packages/backend-php/app/security/PrivilegedAccess.php similarity index 68% rename from app/security/PrivilegedAccess.php rename to packages/backend-php/app/security/PrivilegedAccess.php index 7f8080b23..4dd35832b 100644 --- a/app/security/PrivilegedAccess.php +++ b/packages/backend-php/app/security/PrivilegedAccess.php @@ -14,18 +14,36 @@ use vhs\database\Table; use vhs\security\CurrentUser; +/** @typescript */ class PrivilegedAccess implements IAccess { /** @var IAccess[] */ protected $checks; + /** @var Column */ protected $ownerColumn; - public function __construct(Column $ownerColumn = null) { + /** + * __construct. + * + * @param \vhs\database\Column $ownerColumn + * + * @return void + */ + public function __construct(?Column $ownerColumn = null) { $this->ownerColumn = $ownerColumn; $this->checks = []; } - public static function GenerateAccess($key, Table $table, Column $ownerColumn = null) { + /** + * GenerateAccess. + * + * @param mixed $key + * @param \vhs\database\Table $table + * @param \vhs\database\Column $ownerColumn + * + * @return \vhs\database\access\IAccess + */ + public static function GenerateAccess($key, Table $table, ?Column $ownerColumn = null) { $access = null; $child = null; @@ -43,6 +61,15 @@ public static function GenerateAccess($key, Table $table, Column $ownerColumn = return $access; } + /** + * CanRead. + * + * @param mixed $record + * @param \vhs\database\Table $table + * @param \vhs\database\Column $column + * + * @return bool + */ public function CanRead($record, Table $table, Column $column) { $access = false; foreach ($this->checks as $check) { @@ -52,6 +79,15 @@ public function CanRead($record, Table $table, Column $column) { return $access; } + /** + * CanWrite. + * + * @param mixed $record + * @param \vhs\database\Table $table + * @param \vhs\database\Column $column + * + * @return bool + */ public function CanWrite($record, Table $table, Column $column) { $access = false; foreach ($this->checks as $check) { @@ -61,13 +97,30 @@ public function CanWrite($record, Table $table, Column $column) { return $access; } + /** + * Column. + * + * @param \vhs\database\Column $column + * @param string ...$privileges + * + * @return \vhs\database\access\IAccess + */ public function Column(Column $column, ...$privileges) { $access = new ColumnPrivilegedAccess($this->ownerColumn, $column, ...$privileges); + $this->Register($access); return $access; } + /** + * hasPrivilegedAccess. + * + * @param mixed $record + * @param string ...$privileges + * + * @return bool + */ public function hasPrivilegedAccess($record, ...$privileges) { if (CurrentUser::hasAnyPermissions('administrator')) { return true; @@ -77,9 +130,16 @@ public function hasPrivilegedAccess($record, ...$privileges) { return true; } - return CurrentUser::hasAnyPermissions($privileges); + return CurrentUser::hasAnyPermissions(...$privileges); } + /** + * IsOwner. + * + * @param mixed $record + * + * @return bool + */ public function IsOwner($record) { return array_key_exists($this->ownerColumn->name, $record) && $record[$this->ownerColumn->name] === CurrentUser::getIdentity(); } @@ -106,6 +166,13 @@ public function jsonSerialize(): mixed { ]; } + /** + * Register. + * + * @param \vhs\database\access\IAccess ...$checks + * + * @return void + */ public function Register(IAccess ...$checks) { foreach ($checks as $check) { array_push($this->checks, $check); @@ -121,20 +188,21 @@ public function Register(IAccess ...$checks) { * * @since 5.1.0 */ - public function serialize(): mixed { - return [ - 'type' => 'ownership', - 'ownership' => [ - 'table' => $this->ownerColumn->table->name, - 'name' => $this->ownerColumn->name, - 'type' => $this->ownerColumn->type - ], - 'checks' => $this->checks - ]; + public function serialize(): string { + return json_encode($this->__serialize()); } + /** + * Table. + * + * @param \vhs\database\Table $table + * @param string ...$privileges + * + * @return \app\security\TablePrivilegedAccess + */ public function Table(Table $table, ...$privileges) { $access = new TablePrivilegedAccess($this->ownerColumn, $table, ...$privileges); + $this->Register($access); return $access; @@ -157,10 +225,30 @@ public function unserialize($serialized) { // TODO: Implement unserialize() method. } - public function __serialize() { - return $this->serialize(); + /** + * __serialize. + * + * @return array + */ + public function __serialize(): array { + return [ + 'type' => 'ownership', + 'ownership' => [ + 'table' => $this->ownerColumn->table->name, + 'name' => $this->ownerColumn->name, + 'type' => $this->ownerColumn->type + ], + 'checks' => $this->checks + ]; } + /** + * __unserialize. + * + * @param mixed $data + * + * @return void + */ public function __unserialize($data): void { // TODO: Implement __unserialize() method. } diff --git a/app/security/TablePrivilegedAccess.php b/packages/backend-php/app/security/TablePrivilegedAccess.php similarity index 57% rename from app/security/TablePrivilegedAccess.php rename to packages/backend-php/app/security/TablePrivilegedAccess.php index dd5ef1367..2bbf7e3ba 100644 --- a/app/security/TablePrivilegedAccess.php +++ b/packages/backend-php/app/security/TablePrivilegedAccess.php @@ -12,26 +12,60 @@ use vhs\database\Column; use vhs\database\Table; +/** @typescript */ class TablePrivilegedAccess extends PrivilegedAccess { /** @var string[] */ private $privileges; - /** @var Table */ + + /** @var \vhs\database\Table */ private $table; + /** + * __construct. + * + * @param \vhs\database\Column|null $ownerColumn + * @param \vhs\database\Table $table + * @param string ...$privileges + * + * @return void + */ public function __construct(Column $ownerColumn = null, Table $table, ...$privileges) { parent::__construct($ownerColumn); $this->table = $table; $this->privileges = $privileges; } + /** + * CanRead. + * + * @param mixed $record + * @param \vhs\database\Table $table + * @param \vhs\database\Column $column + * + * @return bool + */ public function CanRead($record, Table $table, Column $column) { return $table === $this->table && $this->hasPrivilegedAccess($record, ...$this->privileges); } + /** + * CanWrite. + * + * @param mixed $record + * @param \vhs\database\Table $table + * @param \vhs\database\Column $column + * + * @return bool + */ public function CanWrite($record, Table $table, Column $column) { return $table === $this->table && $this->hasPrivilegedAccess($record, ...$this->privileges); } + /** + * jsonSerialize. + * + * @return mixed + */ public function jsonSerialize(): mixed { return [ 'type' => 'table', @@ -41,7 +75,21 @@ public function jsonSerialize(): mixed { ]; } - public function serialize(): mixed { + /** + * serialize. + * + * @return string + */ + public function serialize(): string { + return json_encode($this->__serialize()); + } + + /** + * __serialize. + * + * @return array + */ + public function __serialize(): array { return [ 'type' => 'table', 'table' => $this->table->name, @@ -50,10 +98,6 @@ public function serialize(): mixed { ]; } - public function __serialize() { - return $this->serialize(); - } - public function __unserialize($data): void { // TODO maybe implement? } diff --git a/app/security/TokenPrincipal.php b/packages/backend-php/app/security/TokenPrincipal.php similarity index 55% rename from app/security/TokenPrincipal.php rename to packages/backend-php/app/security/TokenPrincipal.php index dcde6cd1c..c08eeccc0 100644 --- a/app/security/TokenPrincipal.php +++ b/packages/backend-php/app/security/TokenPrincipal.php @@ -11,12 +11,43 @@ use vhs\security\IPrincipal; +/** @typescript */ class TokenPrincipal implements IPrincipal { + /** + * grants. + * + * @var mixed + */ private $grants; + /** + * id. + * + * @var mixed + */ private $id; + /** + * name. + * + * @var mixed + */ private $name; + /** + * permissions. + * + * @var mixed + */ private $permissions; + /** + * __construct. + * + * @param mixed $id + * @param mixed $permissions + * @param mixed $grants + * @param mixed $name + * + * @return void + */ public function __construct($id, $permissions, $grants = null, $name = null) { $this->id = $id; $this->permissions = $permissions; @@ -24,30 +55,73 @@ public function __construct($id, $permissions, $grants = null, $name = null) { $this->name = $name; } + /** + * canGrantAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ public function canGrantAllPermissions(...$permission) { return in_array('*', $this->grants) || count(array_diff($permission, $this->grants)) == 0; } + /** + * canGrantAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ public function canGrantAnyPermissions(...$permission) { return in_array('*', $this->grants) || count(array_intersect($permission, $this->grants)) > 0; } + /** + * getIdentity. + * + * @return mixed + */ public function getIdentity() { return $this->id; } + /** + * hasAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ public function hasAllPermissions(...$permission) { return count(array_diff($permission, $this->permissions)) == 0; } + /** + * hasAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ public function hasAnyPermissions(...$permission) { return count(array_intersect($permission, $this->permissions)) > 0; } + /** + * isAnon. + * + * @return bool + */ public function isAnon() { return false; } + /** + * __toString. + * + * @return string + */ public function __toString() { return $this->name; } diff --git a/app/security/UserPrincipal.php b/packages/backend-php/app/security/UserPrincipal.php similarity index 57% rename from app/security/UserPrincipal.php rename to packages/backend-php/app/security/UserPrincipal.php index c29835936..122e31f13 100644 --- a/app/security/UserPrincipal.php +++ b/packages/backend-php/app/security/UserPrincipal.php @@ -11,12 +11,46 @@ use vhs\security\IPrincipal; +/** @typescript */ class UserPrincipal implements IPrincipal, \JsonSerializable { + /** + * grants. + * + * @var mixed + */ private $grants; + + /** + * id. + * + * @var mixed + */ private $id; + + /** + * permissions. + * + * @var mixed + */ private $permissions; + + /** + * username. + * + * @var mixed + */ private $username; + /** + * __construct. + * + * @param mixed $id + * @param mixed $permissions + * @param mixed $grants + * @param mixed $username + * + * @return void + */ public function __construct($id, $permissions, $grants = null, $username = null) { $this->id = $id; $this->permissions = $permissions; @@ -24,30 +58,73 @@ public function __construct($id, $permissions, $grants = null, $username = null) $this->username = $username; } + /** + * canGrantAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ public function canGrantAllPermissions(...$permission) { return in_array('*', $this->grants) || count(array_diff($permission, $this->grants)) == 0; } + /** + * canGrantAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ public function canGrantAnyPermissions(...$permission) { return in_array('*', $this->grants) || count(array_intersect($permission, $this->grants)) > 0; } + /** + * getIdentity. + * + * @return mixed + */ public function getIdentity() { return $this->id; } + /** + * hasAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ public function hasAllPermissions(...$permission) { return count(array_diff($permission, $this->permissions)) == 0; } + /** + * hasAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ public function hasAnyPermissions(...$permission) { return count(array_intersect($permission, $this->permissions)) > 0; } + /** + * isAnon. + * + * @return bool + */ public function isAnon() { return false; } + /** + * jsonSerialize. + * + * @return mixed + */ public function jsonSerialize(): mixed { $data = []; $data['id'] = $this->id; @@ -56,6 +133,11 @@ public function jsonSerialize(): mixed { return $data; } + /** + * __toString. + * + * @return string + */ public function __toString() { return 'user:' . $this->username; } diff --git a/app/security/credentials/ApiCredentials.php b/packages/backend-php/app/security/credentials/ApiCredentials.php similarity index 92% rename from app/security/credentials/ApiCredentials.php rename to packages/backend-php/app/security/credentials/ApiCredentials.php index 9a857a160..35de9b5b2 100644 --- a/app/security/credentials/ApiCredentials.php +++ b/packages/backend-php/app/security/credentials/ApiCredentials.php @@ -9,6 +9,7 @@ namespace app\security\credentials; +/** @typescript */ class ApiCredentials extends TokenCredentials { public function getType() { return 'api'; diff --git a/app/security/credentials/PinCredentials.php b/packages/backend-php/app/security/credentials/PinCredentials.php similarity index 92% rename from app/security/credentials/PinCredentials.php rename to packages/backend-php/app/security/credentials/PinCredentials.php index ff5ea8296..793cc7da9 100644 --- a/app/security/credentials/PinCredentials.php +++ b/packages/backend-php/app/security/credentials/PinCredentials.php @@ -9,6 +9,7 @@ namespace app\security\credentials; +/** @typescript */ class PinCredentials extends TokenCredentials { public function getType() { return 'pin'; diff --git a/app/security/credentials/RfidCredentials.php b/packages/backend-php/app/security/credentials/RfidCredentials.php similarity index 92% rename from app/security/credentials/RfidCredentials.php rename to packages/backend-php/app/security/credentials/RfidCredentials.php index 66797fbb4..138885edf 100644 --- a/app/security/credentials/RfidCredentials.php +++ b/packages/backend-php/app/security/credentials/RfidCredentials.php @@ -9,6 +9,7 @@ namespace app\security\credentials; +/** @typescript */ class RfidCredentials extends TokenCredentials { public function getType() { return 'rfid'; diff --git a/app/security/credentials/TokenCredentials.php b/packages/backend-php/app/security/credentials/TokenCredentials.php similarity index 60% rename from app/security/credentials/TokenCredentials.php rename to packages/backend-php/app/security/credentials/TokenCredentials.php index f3dcc2fee..63eb91e53 100644 --- a/app/security/credentials/TokenCredentials.php +++ b/packages/backend-php/app/security/credentials/TokenCredentials.php @@ -11,15 +11,33 @@ use vhs\security\ICredentials; +/** @typescript */ abstract class TokenCredentials implements ICredentials { - private $token; + private string $token; + /** + * __construct. + * + * @param string $token + * + * @return void + */ public function __construct($token) { $this->token = $token; } + /** + * getType. + * + * @return string + */ abstract public function getType(); + /** + * getToken. + * + * @return string + */ public function getToken() { return $this->token; } diff --git a/app/security/oauth/OAuthHelper.php b/packages/backend-php/app/security/oauth/OAuthHelper.php similarity index 71% rename from app/security/oauth/OAuthHelper.php rename to packages/backend-php/app/security/oauth/OAuthHelper.php index cd4b79936..31608ec60 100644 --- a/app/security/oauth/OAuthHelper.php +++ b/packages/backend-php/app/security/oauth/OAuthHelper.php @@ -17,19 +17,40 @@ use vhs\security\CurrentUser; use vhs\web\HttpServer; +/** @typescript */ class OAuthHelper { /** @var AbstractProvider */ private $provider; + /** @var HttpServer */ private $server; + + /** + * userDetails. + * + * @var mixed + */ private $userDetails; + /** + * __construct. + * + * @param \League\OAuth2\Client\Provider\AbstractProvider $provider + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function __construct(AbstractProvider $provider, HttpServer $server) { $this->provider = $provider; $this->userDetails = null; $this->server = $server; } + /** + * redirectHost. + * + * @return string + */ public static function redirectHost() { $protocol = defined('NOMOS_FORCE_HTTPS') || ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443) @@ -40,9 +61,19 @@ public static function redirectHost() { return $protocol . $domainName; } + /** + * linkAccount. + * + * @param mixed $serviceUID + * @param mixed $serviceType + * @param mixed $notes + * + * @return void + */ public function linkAccount($serviceUID, $serviceType, $notes) { if (!Authenticate::isAuthenticated()) { print 'Not logged in'; + exit(); } @@ -60,18 +91,30 @@ public function linkAccount($serviceUID, $serviceType, $notes) { $key->type = $serviceType; $key->notes = $notes; + // TODO fix typing + /** @disregard P1006 override */ $key->privileges->clear(); + $key->save(); + // TODO fix typing + /** @disregard P1006 override */ $key->privileges->add(Privilege::findByCode('inherit')); $key->save(); } + /** + * processToken. + * + * @return \League\OAuth2\Client\Provider\ResourceOwnerInterface|null + */ public function processToken() { + /** @var \League\OAuth2\Client\Token\AccessToken */ $token = $this->provider->getAccessToken('authorization_code', [ 'code' => $_GET['code'] ]); + if (!is_null($token)) { $this->userDetails = $this->provider->getResourceOwner($token); @@ -81,6 +124,15 @@ public function processToken() { return null; } + /** + * requestAuth. + * + * @param array $options + * + * @return void + * + * @phpstan-ignore missingType.iterableValue + */ public function requestAuth(array $options = []) { // If we don't have an authorization code then get one $authUrl = $this->provider->getAuthorizationUrl($options); diff --git a/app/security/oauth/modules/GithubOAuthHandler.php b/packages/backend-php/app/security/oauth/modules/GithubOAuthHandler.php similarity index 79% rename from app/security/oauth/modules/GithubOAuthHandler.php rename to packages/backend-php/app/security/oauth/modules/GithubOAuthHandler.php index 1d0e2cf11..685b40f4a 100644 --- a/app/security/oauth/modules/GithubOAuthHandler.php +++ b/packages/backend-php/app/security/oauth/modules/GithubOAuthHandler.php @@ -13,11 +13,24 @@ use League\OAuth2\Client\Provider\Github; use vhs\web\HttpServer; +/** @typescript */ class GithubOAuthHandler extends OAuthHandler { + /** + * getUrl. + * + * @return string + */ public function getUrl() { return '/oauth/github.php'; } + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function handle(HttpServer $server) { $host = OauthHelper::redirectHost(); @@ -37,11 +50,11 @@ public function handle(HttpServer $server) { if (!isset($_GET['code'])) { $oauthHelper->requestAuth(); } else { - /** @var GithubResourceOwner | null */ + /** @var \League\OAuth2\Client\Provider\GithubResourceOwner */ $userDetails = $oauthHelper->processToken(); } - if ($_GET['action'] == 'link' && !is_null($userDetails)) { + if ($_GET['action'] == 'link' && $userDetails !== null) { $oauthHelper->linkAccount($userDetails->getId(), 'github', 'GitHub Account for ' . $userDetails->getNickname()); $server->clear(); diff --git a/app/security/oauth/modules/GoogleOAuthHandler.php b/packages/backend-php/app/security/oauth/modules/GoogleOAuthHandler.php similarity index 83% rename from app/security/oauth/modules/GoogleOAuthHandler.php rename to packages/backend-php/app/security/oauth/modules/GoogleOAuthHandler.php index 3bd6ab0dc..acb8c6fce 100644 --- a/app/security/oauth/modules/GoogleOAuthHandler.php +++ b/packages/backend-php/app/security/oauth/modules/GoogleOAuthHandler.php @@ -13,11 +13,24 @@ use League\OAuth2\Client\Provider\Google; use vhs\web\HttpServer; +/** @typescript */ class GoogleOAuthHandler extends OAuthHandler { + /** + * getUrl. + * + * @return string + */ public function getUrl() { return '/oauth/google.php'; } + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function handle(HttpServer $server) { $host = OauthHelper::redirectHost(); @@ -37,7 +50,7 @@ public function handle(HttpServer $server) { if (!isset($_GET['code'])) { $oauthHelper->requestAuth(); } else { - /** @var GoogleUser | null */ + /** @var \League\OAuth2\Client\Provider\GoogleUser|null */ $userDetails = $oauthHelper->processToken(); } diff --git a/app/security/oauth/modules/OAuthHandler.php b/packages/backend-php/app/security/oauth/modules/OAuthHandler.php similarity index 77% rename from app/security/oauth/modules/OAuthHandler.php rename to packages/backend-php/app/security/oauth/modules/OAuthHandler.php index 3521f165e..ba6ffb0c6 100644 --- a/app/security/oauth/modules/OAuthHandler.php +++ b/packages/backend-php/app/security/oauth/modules/OAuthHandler.php @@ -11,6 +11,12 @@ use vhs\web\HttpRequestHandler; +/** @typescript */ abstract class OAuthHandler extends HttpRequestHandler { + /** + * getUrl. + * + * @return string + */ abstract public function getUrl(); } diff --git a/app/security/oauth/modules/OAuthHandlerModule.php b/packages/backend-php/app/security/oauth/modules/OAuthHandlerModule.php similarity index 79% rename from app/security/oauth/modules/OAuthHandlerModule.php rename to packages/backend-php/app/security/oauth/modules/OAuthHandlerModule.php index a86c32049..20f34c5c7 100644 --- a/app/security/oauth/modules/OAuthHandlerModule.php +++ b/packages/backend-php/app/security/oauth/modules/OAuthHandlerModule.php @@ -11,6 +11,7 @@ use vhs\web\modules\HttpRequestHandlerModule; +/** @typescript */ class OAuthHandlerModule extends HttpRequestHandlerModule { /** * @return OAuthHandlerModule @@ -27,6 +28,13 @@ final public static function getInstance() { return $aoInstance[$class]; } + /** + * register. + * + * @param \app\security\oauth\modules\OAuthHandler $handler + * + * @return void + */ public static function register(OAuthHandler $handler) { $url = $handler->getUrl(); @@ -36,6 +44,11 @@ public static function register(OAuthHandler $handler) { self::getInstance()->register_internal('PUT', $url, $handler); } - private function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { } } diff --git a/app/security/oauth/modules/SlackOAuthHandler.php b/packages/backend-php/app/security/oauth/modules/SlackOAuthHandler.php similarity index 85% rename from app/security/oauth/modules/SlackOAuthHandler.php rename to packages/backend-php/app/security/oauth/modules/SlackOAuthHandler.php index 392238208..bc4cee408 100644 --- a/app/security/oauth/modules/SlackOAuthHandler.php +++ b/packages/backend-php/app/security/oauth/modules/SlackOAuthHandler.php @@ -13,11 +13,24 @@ use app\security\oauth\providers\slack\Slack; use vhs\web\HttpServer; +/** @typescript */ class SlackOAuthHandler extends OAuthHandler { + /** + * getUrl. + * + * @return string + */ public function getUrl() { return '/oauth/slack.php'; } + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function handle(HttpServer $server) { $host = OauthHelper::redirectHost(); @@ -42,7 +55,7 @@ public function handle(HttpServer $server) { if (!isset($_GET['code'])) { $oauthHelper->requestAuth(['team' => OAUTH_SLACK_TEAM, 'user_scope' => 'identify']); } else { - /** @var SlackResourceOwner | null */ + /** @var \app\security\oauth\providers\slack\SlackUser */ $userDetails = $oauthHelper->processToken(); } diff --git a/app/security/oauth/providers/slack/Slack.php b/packages/backend-php/app/security/oauth/providers/slack/Slack.php similarity index 50% rename from app/security/oauth/providers/slack/Slack.php rename to packages/backend-php/app/security/oauth/providers/slack/Slack.php index b652b337c..94feea4aa 100644 --- a/app/security/oauth/providers/slack/Slack.php +++ b/packages/backend-php/app/security/oauth/providers/slack/Slack.php @@ -9,44 +9,106 @@ namespace app\security\oauth\providers\slack; -use app\security\oauth\providers\slack\SlackProviderException; use League\OAuth2\Client\Grant\AbstractGrant; use League\OAuth2\Client\Provider\AbstractProvider; use League\OAuth2\Client\Provider\ResourceOwnerInterface; use League\OAuth2\Client\Token\AccessToken; use Psr\Http\Message\ResponseInterface; +/** @typescript */ class Slack extends AbstractProvider { public const ACCESS_TOKEN_RESOURCE_OWNER_ID = 'authed_user'; + /** + * authorizationHeader. + * + * @var string + */ public $authorizationHeader = 'token'; + /** + * domain. + * + * @var string + */ public $domain = 'https://vanhack.slack.com'; + /** + * team. + * + * @var string + */ public $team = ''; + /** + * createResourceOwner. + * + * @param mixed $response + * @param \League\OAuth2\Client\Token\AccessToken $token + * + * @return ResourceOwnerInterface + */ public function createResourceOwner($response, AccessToken $token): ResourceOwnerInterface { return new SlackUser($response); } - public function getBaseAccessTokenUrl(array $params): string { + /** + * getBaseAccessTokenUrl. + * + * @param mixed $params + * + * @return string + */ + public function getBaseAccessTokenUrl(mixed $params): string { return $this->domain . '/api/oauth.v2.access'; } + /** + * getBaseAuthorizationUrl. + * + * @return string + */ public function getBaseAuthorizationUrl() { return $this->domain . '/oauth/v2/authorize'; } + /** + * getResourceOwnerDetailsUrl. + * + * @param \League\OAuth2\Client\Token\AccessToken $token + * + * @return string + */ public function getResourceOwnerDetailsUrl(AccessToken $token) { return $this->domain . '/api/auth.test?token=' . $token; } + /** + * checkResponse. + * + * @param \Psr\Http\Message\ResponseInterface $response + * @param array|string $data + * + * @throws \app\security\oauth\providers\slack\SlackProviderException + * + * @return void + * + * @phpstan-ignore missingType.iterableValue + */ protected function checkResponse(ResponseInterface $response, $data) { if (isset($data['ok']) && $data['ok'] !== true) { - return SlackProviderException::fromResponse($response, $data['error']); + SlackProviderException::fromResponse($response, $data['error']); } } + /** + * createAccessToken. + * + * @param array $response + * @param \League\OAuth2\Client\Grant\AbstractGrant $grant + * + * @return \League\OAuth2\Client\Token\AccessToken + */ protected function createAccessToken(array $response, AbstractGrant $grant) { if (isset($response['authed_user'])) { return new AccessToken($response['authed_user']); @@ -55,6 +117,11 @@ protected function createAccessToken(array $response, AbstractGrant $grant) { return new AccessToken($response['authed_user']); } + /** + * getDefaultScopes. + * + * @return string[] + */ protected function getDefaultScopes() { return []; } diff --git a/packages/backend-php/app/security/oauth/providers/slack/SlackProviderException.php b/packages/backend-php/app/security/oauth/providers/slack/SlackProviderException.php new file mode 100644 index 000000000..ed4e8bde1 --- /dev/null +++ b/packages/backend-php/app/security/oauth/providers/slack/SlackProviderException.php @@ -0,0 +1,23 @@ +getStatusCode(), (string) $response->getBody()); + } +} diff --git a/app/security/oauth/providers/slack/SlackUser.php b/packages/backend-php/app/security/oauth/providers/slack/SlackUser.php similarity index 52% rename from app/security/oauth/providers/slack/SlackUser.php rename to packages/backend-php/app/security/oauth/providers/slack/SlackUser.php index f4efa8601..b47513b6e 100644 --- a/app/security/oauth/providers/slack/SlackUser.php +++ b/packages/backend-php/app/security/oauth/providers/slack/SlackUser.php @@ -4,21 +4,49 @@ use League\OAuth2\Client\Provider\ResourceOwnerInterface; +/** @typescript */ class SlackUser implements ResourceOwnerInterface { + /** + * response. + * + * @var array + */ protected $response; + /** + * __construct. + * + * @param array $response + * + * @return void + */ public function __construct(array $response) { $this->response = $response; } + /** + * getId. + * + * @return mixed + */ public function getId() { - $this->response['user_id']; + return $this->response['user_id']; } + /** + * getName. + * + * @return string + */ public function getName(): string { return $this->response['user']; } + /** + * toArray. + * + * @return array + */ public function toArray() { return $this->response; } diff --git a/app/services/ApiKeyService.php b/packages/backend-php/app/services/ApiKeyService.php similarity index 76% rename from app/services/ApiKeyService.php rename to packages/backend-php/app/services/ApiKeyService.php index 873d2d559..81fd115d6 100644 --- a/app/services/ApiKeyService.php +++ b/packages/backend-php/app/services/ApiKeyService.php @@ -22,11 +22,14 @@ class ApiKeyService extends Service implements IApiKeyService1 { /** * @permission administrator|user * - * @param $keyid + * @param int $keyid * - * @return mixed + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return void */ public function DeleteApiKey($keyid) { + /** @var \app\domain\Key */ $key = Key::find($keyid); if (!CurrentUser::hasAnyPermissions('administrator') && $key->userid != CurrentUser::getIdentity()) { @@ -39,12 +42,13 @@ public function DeleteApiKey($keyid) { /** * @permission administrator * - * @param $notes + * @param string $notes * * @return mixed */ public function GenerateSystemApiKey($notes) { $apiKey = new Key(); + $apiKey->key = bin2hex(openssl_random_pseudo_bytes(32)); $apiKey->type = 'api'; $apiKey->notes = $notes; @@ -56,8 +60,11 @@ public function GenerateSystemApiKey($notes) { /** * @permission administrator|user * - * @param $userid - * @param $notes + * @param int $userid + * @param string $notes + * + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException * * @return mixed */ @@ -73,11 +80,13 @@ public function GenerateUserApiKey($userid, $notes) { } $apiKey = new Key(); + $apiKey->key = bin2hex(openssl_random_pseudo_bytes(32)); $apiKey->type = 'api'; $apiKey->notes = $notes; $user->keys->add($apiKey); + $user->save(); return $apiKey; @@ -86,11 +95,15 @@ public function GenerateUserApiKey($userid, $notes) { /** * @permission administrator|user * - * @param $keyid + * @param int $keyid * - * @return mixed + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key */ public function GetApiKey($keyid) { + /** @var \app\domain\Key */ $key = Key::find($keyid); if (is_null($key)) { @@ -118,7 +131,9 @@ public function GetSystemApiKeys() { /** * @permission administrator|user * - * @param $userid + * @param int $userid + * + * @throws \vhs\security\exceptions\UnauthorizedException * * @return mixed */ @@ -133,19 +148,15 @@ public function GetUserApiKeys($userid) { /** * @permission administrator|user * - * @param $keyid - * @param $privileges + * @param int $keyid + * @param string|string[] $privileges * - * @return mixed + * @return void */ public function PutApiKeyPrivileges($keyid, $privileges) { $key = $this->GetApiKey($keyid); - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = explode(',', $privileges); - } + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; $privs = Privilege::findByCodes(...$privArray); @@ -153,8 +164,10 @@ public function PutApiKeyPrivileges($keyid, $privileges) { $key->privileges->remove($priv); } - foreach ($privs as $priv) { - $key->privileges->add($priv); + if (!is_null($privs)) { + foreach ($privs as $priv) { + $key->privileges->add($priv); + } } $key->save(); @@ -163,11 +176,11 @@ public function PutApiKeyPrivileges($keyid, $privileges) { /** * @permission administrator|user * - * @param $keyid - * @param $notes - * @param $expires + * @param int $keyid + * @param string $notes + * @param string $expires * - * @return mixed + * @return void */ public function UpdateApiKey($keyid, $notes, $expires) { $key = $this->GetApiKey($keyid); diff --git a/app/services/AuthService.php b/packages/backend-php/app/services/AuthService.php similarity index 76% rename from app/services/AuthService.php rename to packages/backend-php/app/services/AuthService.php index e220a08c3..fbdbf871e 100644 --- a/app/services/AuthService.php +++ b/packages/backend-php/app/services/AuthService.php @@ -24,19 +24,47 @@ use vhs\database\Database; use vhs\database\queries\QuerySelect; use vhs\database\wheres\Where; +use vhs\domain\Domain; use vhs\domain\Filter; use vhs\security\CurrentUser; use vhs\security\exceptions\UnauthorizedException; use vhs\security\UserPassCredentials; use vhs\services\Service; +/** @typescript */ class AuthService extends Service implements IAuthService1 { + /** + * fill retVal from User result. + * + * @param \app\domain\Key &$key + * @param \app\domain\User &$user + * @param array &$retval + * + * @return bool + */ + private static function parseValidAccount(&$key, &$user, &$retval): bool { + if ($user->valid) { + $retval['valid'] = true; + $retval['userId'] = $user->id; + $retval['username'] = $user->username; + $retval['type'] = $user->membership->code; + $retval['privileges'] = $key->getAbsolutePrivileges(); + + return true; + } else { + $retval['username'] = $user->username; + $retval['message'] = $user->getInvalidReason(); + } + + return false; + } + /** * Check to see if the user pin and account is valid. * * @permission administrator|pin-auth * - * @param $pin + * @param string $pin * * @return mixed */ @@ -88,35 +116,30 @@ public function CheckPin($pin) { return $retval; } - // Fetch user + // Fetch userinfo $user = User::find($key->userid); - // Check if account is active and in good standing, and return result set - if ($user->valid) { - $retval['valid'] = true; - $retval['userId'] = $user->id; - $retval['username'] = $user->username; - $retval['type'] = $user->membership->code; - $retval['privileges'] = $key->getAbsolutePrivileges(); - - $logAccess(true, $user->id); + // Check if we have a user from the key + if ($user == null || !$user instanceof User) { + $logAccess(false); return $retval; - } else { - $retval['username'] = $user->username; - $retval['message'] = $user->getInvalidReason(); } - // Log and return - $logAccess(false, $user->id); + // Check if account is active and in good standing, and return result set + $isValid = self::parseValidAccount($key, $user, $retval); + + // Log + $logAccess($isValid, $user->id); + // Return return $retval; } /** * @permission administrator|rfid-auth * - * @param $rfid + * @param string $rfid * * @return mixed */ @@ -155,27 +178,23 @@ public function CheckRfid($rfid) { return $retval; } - // Fetch user info + // Fetch userinfo $user = User::find($key->userid); - // Check if account is active and in good standing, and return result set - if ($user->valid) { - $retval['valid'] = true; - $retval['userId'] = $user->id; - $retval['username'] = $user->username; - $retval['type'] = $user->membership->code; - $retval['privileges'] = $key->getAbsolutePrivileges(); - - $logAccess(true, $user->id); + // Check if we have a user from the key + if ($user == null || !$user instanceof User) { + $logAccess(false); return $retval; - } else { - $retval['username'] = $user->username; - $retval['message'] = $user->getInvalidReason(); } - $logAccess(false, $user->id); + // Check if account is active and in good standing, and return result set + $isValid = self::parseValidAccount($key, $user, $retval); + + // Log + $logAccess($isValid, $user->id); + // Return return $retval; } @@ -184,8 +203,8 @@ public function CheckRfid($rfid) { * * @permission administrator|service-auth * - * @param $service - * @param $id + * @param string $service + * @param string $id * * @return mixed */ @@ -230,34 +249,29 @@ public function CheckService($service, $id) { // Fetch userinfo $user = User::find($key->userid); - // Check if account is active and in good standing, and return result set - if ($user->valid) { - $retval['valid'] = true; - $retval['userId'] = $user->id; - $retval['username'] = $user->username; - $retval['type'] = $user->membership->code; - $retval['privileges'] = $key->getAbsolutePrivileges(); - - $logAccess(true, $user->id); + // Check if we have a user from the key + if ($user == null || !$user instanceof User) { + $logAccess(false); return $retval; - } else { - $retval['username'] = $user->username; - $retval['message'] = $user->getInvalidReason(); } - // Log and return - $logAccess(false, $user->id); + // Check if account is active and in good standing, and return result set + $isValid = self::parseValidAccount($key, $user, $retval); + + // Log + $logAccess($isValid, $user->id); + // Return return $retval; } /** * @permission anonymous * - * @param $username + * @param string $username * - * @return boolean + * @return bool */ public function CheckUsername($username) { return Database::exists( @@ -268,27 +282,18 @@ public function CheckUsername($username) { /** * @permission administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ public function CountAccessLog($filters) { - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } - return AccessLog::count($filters); } /** * @permission administrator * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return int */ @@ -299,8 +304,8 @@ public function CountClients($filters) { /** * @permission administrator|user * - * @param $userid - * @param $filters + * @param int $userid + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -313,8 +318,8 @@ public function CountUserAccessLog($userid, $filters) { /** * @permission administrator|user * - * @param $userid - * @param $filters + * @param int $userid + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -338,7 +343,7 @@ public function CurrentUser() { /** * @permission administrator|user * - * @param $id + * @param int $id * * @return mixed */ @@ -355,10 +360,10 @@ public function DeleteClient($id) { /** * @permission administrator|user * - * @param $id - * @param $enabled + * @param int $id + * @param bool $enabled * - * @return mixed + * @return void */ public function EnableClient($id, $enabled) { $client = $this->GetMyClient($id); @@ -375,7 +380,7 @@ public function EnableClient($id, $enabled) { /** * @permission oauth-provider * - * @param $bearerToken + * @param string $bearerToken * * @return mixed */ @@ -386,10 +391,10 @@ public function GetAccessToken($bearerToken) { /** * @permission anonymous * - * @param $clientId - * @param $clientSecret + * @param int $clientId + * @param string $clientSecret * - * @return mixed + * @return mixed|null */ public function GetClient($clientId, $clientSecret) { $client = AppClient::find($clientId); @@ -405,7 +410,7 @@ public function GetClient($clientId, $clientSecret) { * @permission oauth-provider * @permission authenticated * - * @param $clientId + * @param int $clientId * * @return mixed */ @@ -422,7 +427,7 @@ public function GetClientInfo($clientId) { /** * @permission oauth-provider * - * @param $refreshToken + * @param string $refreshToken * * @return mixed */ @@ -433,8 +438,8 @@ public function GetRefreshToken($refreshToken) { /** * @permission oauth-provider * - * @param $username - * @param $password + * @param string $username + * @param string $password * * @return mixed */ @@ -445,31 +450,26 @@ public function GetUser($username, $password) { /** * @permission administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * - * @return mixed + * @return \app\domain\AccessLog[] */ public function ListAccessLog($page, $size, $columns, $order, $filters) { - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } - return AccessLog::page($page, $size, $columns, $order, $filters); } /** * @permission administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -480,16 +480,14 @@ public function ListClients($page, $size, $columns, $order, $filters) { /** * @permission administrator|user * - * @param $userid - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed - * - * @throws \Exception */ public function ListUserAccessLog($userid, $page, $size, $columns, $order, $filters) { $filters = $this->AddUserIDToFilters($userid, $filters); @@ -500,25 +498,22 @@ public function ListUserAccessLog($userid, $page, $size, $columns, $order, $filt /** * @permission administrator|user * - * @param $userid - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $userid + * @param int $page + * @param int $size + * @param mixed $columns + * @param mixed $order + * @param string|\vhs\domain\Filter|null $filters * - * @return array + * @throws \vhs\security\exceptions\UnauthorizedException * - * @throws \Exception + * @return AppClient[] */ public function ListUserClients($userid, $page, $size, $columns, $order, $filters) { $userService = new UserService(); $user = $userService->GetUser($userid); - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } + Domain::coerceFilters($filters); if (is_null($user)) { throw new UnauthorizedException('User not found or you do not have access'); @@ -544,8 +539,8 @@ public function ListUserClients($userid, $page, $size, $columns, $order, $filter /** * @permission anonymous * - * @param $username - * @param $password + * @param string $username + * @param string $password * * @return mixed */ @@ -562,7 +557,7 @@ public function Login($username, $password) { /** * @permission user * - * @return mixed + * @return void */ public function Logout() { Authenticate::getInstance()->logout(); @@ -571,9 +566,9 @@ public function Logout() { /** * @permission anonymous * - * @param $pin + * @param string $pin * - * @return mixed + * @return string */ public function PinLogin($pin) { try { @@ -588,10 +583,10 @@ public function PinLogin($pin) { /** * @permission user * - * @param $name - * @param $description - * @param $url - * @param $redirecturi + * @param string $name + * @param string $description + * @param string $url + * @param string $redirecturi * * @return mixed */ @@ -614,7 +609,7 @@ public function RegisterClient($name, $description, $url, $redirecturi) { /** * @permission oauth-provider * - * @param $refreshToken + * @param string $refreshToken * * @return mixed */ @@ -629,9 +624,9 @@ public function RevokeRefreshToken($refreshToken) { /** * @permission anonymous * - * @param $key + * @param string $key * - * @return mixed + * @return string */ public function RfidLogin($key) { try { @@ -646,10 +641,10 @@ public function RfidLogin($key) { /** * @permission oauth-provider * - * @param $userId - * @param $accessToken - * @param $clientId - * @param $expires + * @param int $userId + * @param string $accessToken + * @param int $clientId + * @param string $expires * * @return mixed */ @@ -670,7 +665,7 @@ public function SaveAccessToken($userId, $accessToken, $clientId, $expires) { $token->client = $client; } - $expiry = new \DateTime($expires); + $expiry = new DateTime($expires); $token->expires = $expiry->format('Y-m-d H:i:s'); @@ -682,10 +677,10 @@ public function SaveAccessToken($userId, $accessToken, $clientId, $expires) { /** * @permission oauth-provider * - * @param $userId - * @param $refreshToken - * @param $clientId - * @param $expires + * @param int $userId + * @param string $refreshToken + * @param int $clientId + * @param string $expires * * @return mixed */ @@ -706,7 +701,7 @@ public function SaveRefreshToken($userId, $refreshToken, $clientId, $expires) { $token->client = $client; } - $expiry = new \DateTime($expires); + $expiry = new DateTime($expires); $token->expires = $expiry->format('Y-m-d H:i:s'); @@ -715,14 +710,21 @@ public function SaveRefreshToken($userId, $refreshToken, $clientId, $expires) { return $this->trimUser($user); } + /** + * AddUserIDToFilters. + * + * @param int $userid + * @param Filter|string $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Filter + */ private function AddUserIDToFilters($userid, $filters) { $userService = new UserService(); $user = $userService->GetUser($userid); - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } + Domain::coerceFilters($filters); if (is_null($user)) { throw new UnauthorizedException('User not found or you do not have access'); @@ -739,6 +741,13 @@ private function AddUserIDToFilters($userid, $filters) { return $filters; } + /** + * GetMyClient. + * + * @param int $id + * + * @return AppClient|null + */ private function GetMyClient($id) { $client = AppClient::find($id); @@ -753,6 +762,13 @@ private function GetMyClient($id) { return null; } + /** + * trimClient. + * + * @param AppClient $client + * + * @return array|null + */ private function trimClient($client) { if (is_null($client)) { return null; @@ -770,6 +786,13 @@ private function trimClient($client) { ]; } + /** + * trimClientInfo. + * + * @param AppClient|null $client + * + * @return array|null + */ private function trimClientInfo($client) { if (is_null($client)) { return null; @@ -783,6 +806,13 @@ private function trimClientInfo($client) { ]; } + /** + * trimUser. + * + * @param User|null $user + * + * @return array|null + */ private function trimUser($user) { if (is_null($user)) { return null; diff --git a/app/services/EmailService.php b/packages/backend-php/app/services/EmailService.php similarity index 66% rename from app/services/EmailService.php rename to packages/backend-php/app/services/EmailService.php index c2a0747b0..2e7fee464 100644 --- a/app/services/EmailService.php +++ b/packages/backend-php/app/services/EmailService.php @@ -5,12 +5,13 @@ use app\contracts\IEmailService1; use app\domain\EmailTemplate; use Aws\Ses\SesClient; +use vhs\services\Service; -class EmailService implements IEmailService1 { +class EmailService extends Service implements IEmailService1 { /** * @permission administrator * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return int */ @@ -21,11 +22,12 @@ public function CountTemplates($filters) { /** * @permission administrator * - * @param $id + * @param int $id * - * @return mixed + * @return void */ public function DeleteTemplate($id) { + /** @var \app\domain\EmailTemplate */ $template = EmailTemplate::find($id); if (!is_null($template)) { @@ -33,6 +35,16 @@ public function DeleteTemplate($id) { } } + /** + * Email. + * + * @param mixed $email + * @param mixed $tmpl + * @param mixed $context + * @param mixed $subject + * + * @return null + */ public function Email($email, $tmpl, $context, $subject = null) { $generated = EmailTemplate::generate($tmpl, $context); @@ -41,7 +53,7 @@ public function Email($email, $tmpl, $context, $subject = null) { } if (is_null($subject)) { - $subject = $generated['subject']; + $subject = $generated->subject; } $client = new SesClient([ @@ -66,11 +78,11 @@ public function Email($email, $tmpl, $context, $subject = null) { 'Body' => [ 'Text' => [ // Data is required - 'Data' => $generated['txt'] + 'Data' => $generated->txt ], 'Html' => [ // Data is required - 'Data' => $generated['html'] + 'Data' => $generated->html ] ] ] @@ -79,6 +91,16 @@ public function Email($email, $tmpl, $context, $subject = null) { return null; } + /** + * EmailUser. + * + * @param mixed $user + * @param mixed $tmpl + * @param mixed $context + * @param mixed $subject + * + * @return void + */ public function EmailUser($user, $tmpl, $context, $subject = null) { $this->Email($user->email, $tmpl, $context, $subject); } @@ -86,40 +108,42 @@ public function EmailUser($user, $tmpl, $context, $subject = null) { /** * @permission administrator * - * @param $id + * @param int $id * - * @return mixed + * @return \app\domain\EmailTemplate */ public function GetTemplate($id) { + /** @var \app\domain\EmailTemplate */ return EmailTemplate::find($id); } /** * @permission administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * - * @return mixed + * @return \app\domain\EmailTemplate[] */ public function ListTemplates($page, $size, $columns, $order, $filters) { + /** @var \app\domain\EmailTemplate[] */ return EmailTemplate::page($page, $size, $columns, $order, $filters); } /** * @permission administrator * - * @param $name - * @param $code - * @param $subject - * @param $help - * @param $body - * @param $html + * @param string $name + * @param string $code + * @param string $subject + * @param string $help + * @param string $body + * @param string $html * - * @return mixed + * @return void */ public function PutTemplate($name, $code, $subject, $help, $body, $html) { $template = EmailTemplate::findByCode($code); @@ -141,84 +165,102 @@ public function PutTemplate($name, $code, $subject, $help, $body, $html) { /** * @permission administrator * - * @param $id - * @param $body + * @param int $id + * @param string $body * - * @return mixed + * @return void */ public function UpdateTemplateBody($id, $body) { + /** @var \app\domain\EmailTemplate */ $template = EmailTemplate::find($id); + $template->body = $body; + $template->save(); } /** * @permission administrator * - * @param $id - * @param $code + * @param int $id + * @param string $code * - * @return mixed + * @return void */ public function UpdateTemplateCode($id, $code) { + /** @var \app\domain\EmailTemplate */ $template = EmailTemplate::find($id); + $template->code = $code; + $template->save(); } /** * @permission administrator * - * @param $id - * @param $help + * @param int $id + * @param string $help * - * @return mixed + * @return void */ public function UpdateTemplateHelp($id, $help) { + /** @var \app\domain\EmailTemplate */ $template = EmailTemplate::find($id); + $template->help = $help; + $template->save(); } /** * @permission administrator * - * @param $id - * @param $html + * @param int $id + * @param string $html * - * @return mixed + * @return void */ public function UpdateTemplateHtml($id, $html) { + /** @var \app\domain\EmailTemplate */ $template = EmailTemplate::find($id); + $template->html = $html; + $template->save(); } /** * @permission administrator * - * @param $id - * @param $name + * @param int $id + * @param string $name * - * @return mixed + * @return void */ public function UpdateTemplateName($id, $name) { + /** @var \app\domain\EmailTemplate */ $template = EmailTemplate::find($id); + $template->name = $name; + $template->save(); } /** * @permission administrator * - * @param $id - * @param $subject + * @param int $id + * @param string $subject * - * @return mixed + * @return void */ public function UpdateTemplateSubject($id, $subject) { + /** @var \app\domain\EmailTemplate */ $template = EmailTemplate::find($id); + $template->subject = $subject; + $template->save(); } } diff --git a/app/services/EventService.php b/packages/backend-php/app/services/EventService.php similarity index 80% rename from app/services/EventService.php rename to packages/backend-php/app/services/EventService.php index 65b77329c..d7baa2973 100644 --- a/app/services/EventService.php +++ b/packages/backend-php/app/services/EventService.php @@ -13,16 +13,15 @@ use app\domain\Event; use app\domain\Privilege; use app\exceptions\InvalidInputException; -use Aws\CloudFront\Exception\Exception; -use vhs\domain\Domain; use vhs\security\CurrentUser; use vhs\services\Service; +/** @typescript */ class EventService extends Service implements IEventService1 { /** * @permission administrator * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return int */ @@ -33,11 +32,13 @@ public function CountEvents($filters) { /** * @permission administrator * - * @param $name - * @param $domain - * @param $event - * @param $description - * @param $enabled + * @param string $name + * @param string $domain + * @param string $event + * @param string $description + * @param bool $enabled + * + * @throws \app\exceptions\InvalidInputException * * @return mixed */ @@ -60,7 +61,7 @@ public function CreateEvent($name, $domain, $event, $description, $enabled) { /** * @permission administrator * - * @param $id + * @param int $id * * @return mixed */ @@ -73,8 +74,8 @@ public function DeleteEvent($id) { /** * @permission administrator * - * @param $id - * @param $enabled + * @param int $id + * @param bool $enabled * * @return mixed */ @@ -117,7 +118,7 @@ public function GetAccessibleEvents() { /** * @permission webhook|administrator * - * @param $domain + * @param string $domain * * @return mixed */ @@ -151,7 +152,9 @@ public function GetDomainDefinitions() { /** * @permission administrator * - * @param $id + * @param int $id + * + * @throws \app\exceptions\InvalidInputException * * @return mixed */ @@ -186,17 +189,17 @@ public function GetEventTypes() { sort($updateKeys); - return array_map(fn($method): string => str_replace('before', 'before:', strtolower(str_replace('onAny', '', $method))), $updateKeys); + return array_map(fn ($method): string => str_replace('before', 'before:', strtolower(str_replace('onAny', '', $method))), $updateKeys); } /** * @permission webhook|administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -207,19 +210,15 @@ public function ListEvents($page, $size, $columns, $order, $filters) { /** * @permission administrator * - * @param $id - * @param $privileges + * @param int $id + * @param string $privileges * - * @return mixed + * @return void */ public function PutEventPrivileges($id, $privileges) { $event = $this->GetEvent($id); - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = explode(',', $privileges); - } + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; $privs = Privilege::findByCodes(...$privArray); @@ -237,12 +236,12 @@ public function PutEventPrivileges($id, $privileges) { /** * @permission administrator * - * @param $id - * @param $name - * @param $domain - * @param $event - * @param $description - * @param $enabled + * @param int $id + * @param string $name + * @param string $domain + * @param string $event + * @param string $description + * @param bool $enabled * * @return mixed */ diff --git a/app/services/IpnService.php b/packages/backend-php/app/services/IpnService.php similarity index 72% rename from app/services/IpnService.php rename to packages/backend-php/app/services/IpnService.php index 47ceca444..df9806761 100644 --- a/app/services/IpnService.php +++ b/packages/backend-php/app/services/IpnService.php @@ -10,7 +10,7 @@ class IpnService extends Service implements IIpnService1 { /** * @permission administrator * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return int */ @@ -21,7 +21,7 @@ public function CountRecords($filters) { /** * @permission administrator * - * @param $ipnId + * @param int $ipnId * * @return mixed */ @@ -41,11 +41,11 @@ public function GetAll() { /** * @permission administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ diff --git a/app/services/KeyService.php b/packages/backend-php/app/services/KeyService.php similarity index 77% rename from app/services/KeyService.php rename to packages/backend-php/app/services/KeyService.php index 04bcb9695..2f937bd84 100644 --- a/app/services/KeyService.php +++ b/packages/backend-php/app/services/KeyService.php @@ -22,11 +22,12 @@ use vhs\security\exceptions\UnauthorizedException; use vhs\services\Service; +/** @typescript */ class KeyService extends Service implements IKeyService1 { /** * @permission administrator|user * - * @param $keyid + * @param int $keyid * * @return mixed */ @@ -39,10 +40,12 @@ public function DeleteKey($keyid) { /** * @permission administrator|user * - * @param $userid - * @param $type - * @param $value - * @param $notes + * @param int $userid + * @param $type + * @param $value + * @param $notes + * + * @throws \app\exceptions\InvalidInputException * * @return mixed */ @@ -56,14 +59,23 @@ public function GenerateUserKey($userid, $type, $value, $notes) { switch ($type) { case 'rfid': $key->key = $value; + break; case 'pin': - $nextpinid = Database::scalar(Query::Select(SettingsSchema::Table(), SettingsSchema::Columns()->nextpinid)); + $nextpinid = Database::scalar( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Query::Select(SettingsSchema::Table(), SettingsSchema::Columns()->nextpinid) + ); $key->key = sprintf('%04s', $nextpinid) . '|' . sprintf('%04s', rand(0, 9999)); + // TODO fix typing + /** @disregard P1006 override */ $key->privileges->add(Privilege::findByCode('inherit')); + break; case 'api': $key->key = bin2hex(openssl_random_pseudo_bytes(32)); + break; default: throw new InvalidInputException('Unsupported key type'); @@ -85,6 +97,8 @@ public function GenerateUserKey($userid, $type, $value, $notes) { /** * @permission administrator * + * @throws \vhs\security\exceptions\UnauthorizedException + * * @return mixed */ public function GetAllKeys() { @@ -98,11 +112,15 @@ public function GetAllKeys() { /** * @permission administrator|user * - * @param $keyid + * @param int $keyid + * + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException * * @return mixed */ public function GetKey($keyid) { + /** @var \app\domain\Key */ $key = Key::find($keyid); if (is_null($key)) { @@ -121,6 +139,8 @@ public function GetKey($keyid) { /** * @permission administrator * + * @throws \vhs\security\exceptions\UnauthorizedException + * * @return mixed */ public function GetSystemKeys() { @@ -128,14 +148,16 @@ public function GetSystemKeys() { throw new UnauthorizedException(); } + // TODO implement proper typing + // @phpstan-ignore property.notFound return Key::where(Where::Null(Key::Schema()->Columns()->userid)); } /** * @permission administrator|user * - * @param $userid - * @param $types + * @param int $userid + * @param $types * * @return mixed */ @@ -160,20 +182,15 @@ public function GetUserKeys($userid, $types) { /** * @permission administrator|user * - * @param $keyid - * @param $privileges + * @param int $keyid + * @param $privileges * * @return mixed */ public function PutKeyPrivileges($keyid, $privileges) { $key = $this->GetKey($keyid); - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = []; - array_push($privArray, $privileges); - } + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; $privs = Privilege::findByCodes(...$privArray); @@ -191,9 +208,9 @@ public function PutKeyPrivileges($keyid, $privileges) { /** * @permission administrator|user * - * @param $keyid - * @param $notes - * @param $expires + * @param int $keyid + * @param $notes + * @param $expires * * @return mixed */ diff --git a/app/services/MemberCardService.php b/packages/backend-php/app/services/MemberCardService.php similarity index 67% rename from app/services/MemberCardService.php rename to packages/backend-php/app/services/MemberCardService.php index f2c6de7b9..a45f7afd0 100644 --- a/app/services/MemberCardService.php +++ b/packages/backend-php/app/services/MemberCardService.php @@ -18,14 +18,17 @@ use vhs\database\Columns; use vhs\database\queries\Query; use vhs\database\wheres\Where; +use vhs\domain\Domain; use vhs\domain\Filter; use vhs\security\exceptions\UnauthorizedException; +use vhs\services\Service; -class MemberCardService implements IMemberCardService1 { +/** @typescript */ +class MemberCardService extends Service implements IMemberCardService1 { /** * @permission administrator * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -36,8 +39,8 @@ public function CountGenuineCards($filters) { /** * @permission administrator * - * @param $userid - * @param $filters + * @param int $userid + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -50,7 +53,7 @@ public function CountGenuineUserCards($userid, $filters) { /** * @permission administrator * - * @param $key + * @param string $key * * @return mixed */ @@ -61,12 +64,13 @@ public function GetGenuineCardDetails($key) { /** * @permission administrator * - * @param $email - * @param $key + * @param string $email + * @param string $key * - * @return mixed + * @throws \app\exceptions\InvalidInputException + * @throws \app\exceptions\MemberCardException * - * @throws \Exception + * @return mixed */ public function IssueCard($email, $key) { $users = User::findByPaymentEmail($email); @@ -84,13 +88,29 @@ public function IssueCard($email, $key) { $payments = Payment::where( Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(Payment::Schema()->Columns()->status, 1), - Where::Equal(Payment::Schema()->Columns()->item_number, 'vhs_card_2015'), //TODO eventually put these into card campaigns or something + // TODO eventually put these into card campaigns or something + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->item_number, 'vhs_card_2015'), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(Payment::Schema()->Columns()->payer_email, $email), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(Payment::Schema()->Columns()->user_id, $user->id), Where::NotIn( + // TODO implement proper typing + // @phpstan-ignore property.notFound Payment::Schema()->Columns()->id, - Query::Select(GenuineCard::Schema()->Table(), new Columns(GenuineCard::Schema()->Columns()->paymentid)) + Query::Select( + GenuineCard::Schema()->Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + new Columns(GenuineCard::Schema()->Columns()->paymentid) + ) ) ) ); @@ -120,11 +140,11 @@ public function IssueCard($email, $key) { /** * @permission administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -135,25 +155,22 @@ public function ListGenuineCards($page, $size, $columns, $order, $filters) { /** * @permission administrator|user * - * @param $userid - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * - * @return mixed + * @throws \vhs\security\exceptions\UnauthorizedException * - * @throws \Exception + * @return mixed */ public function ListUserGenuineCards($userid, $page, $size, $columns, $order, $filters) { $userService = new UserService(); $user = $userService->GetUser($userid); - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } + Domain::coerceFilters($filters); if (is_null($user)) { throw new UnauthorizedException('User not found or you do not have access'); @@ -173,12 +190,12 @@ public function ListUserGenuineCards($userid, $page, $size, $columns, $order, $f /** * @permission administrator * - * @param $key - * @param $notes + * @param string $key + * @param string $notes * - * @return GenuineCard + * @throws \app\exceptions\MemberCardException * - * @throws \Exception + * @return GenuineCard */ public function RegisterGenuineCard($key, $notes) { $keys = GenuineCard::findByKey($key); @@ -200,12 +217,12 @@ public function RegisterGenuineCard($key, $notes) { /** * @permission administrator * - * @param $key - * @param $active + * @param string $key + * @param bool $active * - * @return mixed + * @throws \app\exceptions\InvalidInputException * - * @throws \Exception + * @return mixed */ public function UpdateGenuineCardActive($key, $active) { if (!$this->ValidateGenuineCard($key)) { @@ -224,7 +241,7 @@ public function UpdateGenuineCardActive($key, $active) { /** * @permission user * - * @param $key + * @param string $key * * @return bool */ @@ -234,14 +251,21 @@ public function ValidateGenuineCard($key) { return !is_null($keys) && count($keys) == 1; } + /** + * addUserIDToFilters. + * + * @param int $userid + * @param Filter|string $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Filter + */ private function addUserIDToFilters($userid, $filters) { $userService = new UserService(); $user = $userService->GetUser($userid); - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } + Domain::coerceFilters($filters); if (is_null($user)) { throw new UnauthorizedException('User not found or you do not have access'); diff --git a/app/services/MembershipService.php b/packages/backend-php/app/services/MembershipService.php similarity index 64% rename from app/services/MembershipService.php rename to packages/backend-php/app/services/MembershipService.php index fd422d7e3..9a6299033 100644 --- a/app/services/MembershipService.php +++ b/packages/backend-php/app/services/MembershipService.php @@ -5,13 +5,16 @@ use app\contracts\IMembershipService1; use app\domain\Membership; use app\domain\Privilege; +use vhs\exceptions\HttpException; use vhs\services\Service; +use vhs\web\enums\HttpStatusCodes; +/** @typescript */ class MembershipService extends Service implements IMembershipService1 { /** * @permission administrator * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -22,24 +25,25 @@ public function CountMemberships($filters) { /** * @permission administrator * - * @param $title - * @param $description - * @param $price - * @param $days - * @param $private - * @param $active - * @param $code + * @param string $title + * @param string $description + * @param float $price + * @param string $code + * @param int $days + * @param string $period + * + * @throws \vhs\exceptions\HttpException * * @return mixed */ public function Create($title, $description, $price, $code, $days, $period) { - return []; + throw new HttpException('Sorry, no dice!', HttpStatusCodes::Server_Error_Not_Implemented); } /** * @permission administrator * - * @param $membershipId + * @param int $membershipId * * @return mixed */ @@ -59,11 +63,11 @@ public function GetAll() { /** * @permission administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -73,15 +77,16 @@ public function ListMemberships($page, $size, $columns, $order, $filters) { /** * @permission administrator + * + * @param int $membershipId + * @param string|string[] $privileges + * + * @return void */ public function PutPrivileges($membershipId, $privileges) { $membership = $this->Get($membershipId); - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = explode(',', $privileges); - } + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; $privs = Privilege::findByCodes(...$privArray); @@ -89,8 +94,10 @@ public function PutPrivileges($membershipId, $privileges) { $membership->privileges->remove($priv); } - foreach ($privs as $priv) { - $membership->privileges->add($priv); + if (!empty($privs)) { + foreach ($privs as $priv) { + $membership->privileges->add($priv); + } } $membership->save(); @@ -99,6 +106,14 @@ public function PutPrivileges($membershipId, $privileges) { /** * @permission administrator * + * @param int $membershipId + * @param string $title + * @param string $description + * @param float $price + * @param string $code + * @param int $days + * @param string $period + * * @return mixed */ public function Update($membershipId, $title, $description, $price, $code, $days, $period) { @@ -119,7 +134,10 @@ public function Update($membershipId, $title, $description, $price, $code, $days /** * @permission administrator * - * @return mixed + * @param int $membershipId + * @param bool $active + * + * @return void */ public function UpdateActive($membershipId, $active) { $membership = $this->Get($membershipId); @@ -132,7 +150,10 @@ public function UpdateActive($membershipId, $active) { /** * @permission administrator * - * @return mixed + * @param int $membershipId + * @param bool $private + * + * @return void */ public function UpdatePrivate($membershipId, $private) { $membership = $this->Get($membershipId); @@ -145,7 +166,10 @@ public function UpdatePrivate($membershipId, $private) { /** * @permission administrator * - * @return mixed + * @param int $membershipId + * @param bool $recurring + * + * @return void */ public function UpdateRecurring($membershipId, $recurring) { $membership = $this->Get($membershipId); @@ -158,7 +182,10 @@ public function UpdateRecurring($membershipId, $recurring) { /** * @permission administrator * - * @return mixed + * @param int $membershipId + * @param bool $trial + * + * @return void */ public function UpdateTrial($membershipId, $trial) { $membership = $this->Get($membershipId); diff --git a/app/services/MetricService.php b/packages/backend-php/app/services/MetricService.php similarity index 76% rename from app/services/MetricService.php rename to packages/backend-php/app/services/MetricService.php index 32ac07d30..16822ce73 100644 --- a/app/services/MetricService.php +++ b/packages/backend-php/app/services/MetricService.php @@ -13,12 +13,13 @@ use vhs\database\wheres\Where; use vhs\services\Service; +/** @typescript */ class MetricService extends Service implements IMetricService1 { /** * Get the total new members recorded in the date range. * - * @param $start int unixtime - * @param $end int unixtime + * @param int $start int unixtime + * @param int $end int unixtime * * @return int */ @@ -26,9 +27,17 @@ protected static function NewMemberCount($start, $end) { $query = Query::count( UserSchema::Table(), Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(UserSchema::Columns()->active, 'y'), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(UserSchema::Columns()->mem_expire, date('Y-m-d H:i:s')), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::LesserEqual(UserSchema::Columns()->created, date('Y-m-d 00:00:00', $end)), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(UserSchema::Columns()->created, date('Y-m-d 00:00:00', $start)) ) ); @@ -39,9 +48,9 @@ protected static function NewMemberCount($start, $end) { /** * Get the total new memberships of a type recorded in the date range. * - * @param $membership_id int - * @param $start int unixtime - * @param $end int unixtime + * @param int $membership_id int + * @param int $start int unixtime + * @param int $end int unixtime * * @return int */ @@ -49,10 +58,20 @@ protected static function NewMembershipByIdCount($membership_id, $start, $end) { $query = Query::count( UserSchema::Table(), Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(UserSchema::Columns()->active, 'y'), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(UserSchema::Columns()->mem_expire, date('Y-m-d H:i:s')), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(UserSchema::Columns()->membership_id, $membership_id), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::LesserEqual(UserSchema::Columns()->created, date('Y-m-d 00:00:00', $end)), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(UserSchema::Columns()->created, date('Y-m-d 00:00:00', $start)) ) ); @@ -63,9 +82,6 @@ protected static function NewMembershipByIdCount($membership_id, $start, $end) { /** * Get the total members. * - * @param $start int unixtime - * @param $end int unixtime - * * @return int */ protected static function TotalMemberCount() { @@ -73,20 +89,37 @@ protected static function TotalMemberCount() { Query::count( UserSchema::Table(), Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(UserSchema::Columns()->active, 'y'), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(UserSchema::Columns()->mem_expire, date('Y-m-d H:i:s')) ) ) ); } + /** + * TotalMembershipByIdCount. + * + * @param int $membership_id + * + * @return int + */ protected static function TotalMembershipByIdCount($membership_id) { return Database::count( Query::count( UserSchema::Table(), Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(UserSchema::Columns()->active, 'y'), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(UserSchema::Columns()->mem_expire, date('Y-m-d H:i:s')), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(UserSchema::Columns()->membership_id, $membership_id) ) ) @@ -96,15 +129,19 @@ protected static function TotalMembershipByIdCount($membership_id) { /** * @permission user * - * @param $start_range - * @param $end_range + * @param string $start_range + * @param string $end_range * * @return mixed */ public function GetCreatedDates($start_range, $end_range) { $users = User::where( Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(User::Schema()->Columns()->created, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::LesserEqual(User::Schema()->Columns()->created, $end_range) ) ); @@ -161,31 +198,45 @@ public function GetCreatedDates($start_range, $end_range) { * @return mixed */ public function GetExceptionPayments() { + // TODO implement proper typing + // @phpstan-ignore property.notFound return Payment::where(Where::NotEqual(Payment::Schema()->Columns()->status, 1)); } /** * @permission user * - * @param $start_range - * @param $end_range - * @param $group + * @param string $start_range + * @param string $end_range + * @param string $group * * @return mixed */ public function GetMembers($start_range, $end_range, $group) { $users = User::where( Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(User::Schema()->Columns()->created, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::LesserEqual(User::Schema()->Columns()->created, $end_range) ) ); $payments = Payment::where( Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(Payment::Schema()->Columns()->status, 1), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(Payment::Schema()->Columns()->date, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::LesserEqual(Payment::Schema()->Columns()->date, $end_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Like(Payment::Schema()->Columns()->item_number, 'vhs_membership_%') ) ); @@ -266,6 +317,8 @@ public function GetNewMembers($start_range, $end_range) { * @return mixed */ public function GetPendingAccounts() { + // TODO implement proper typing + // @phpstan-ignore property.notFound return User::where(Where::Equal(User::Schema()->Columns()->active, 't')); } @@ -281,8 +334,14 @@ public function GetPendingAccounts() { public function GetRevenue($start_range, $end_range, $group) { $payments = Payment::where( Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(Payment::Schema()->Columns()->status, 1), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(Payment::Schema()->Columns()->date, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::LesserEqual(Payment::Schema()->Columns()->date, $end_range) ) ); @@ -301,15 +360,19 @@ public function GetRevenue($start_range, $end_range, $group) { switch ($group) { case 'day': $grouping = $grouping->format('Y-m-d'); + break; case 'month': $grouping = $grouping->format('Y-m'); + break; case 'year': $grouping = $grouping->format('Y'); + break; default: $grouping = 'all'; + break; } @@ -361,6 +424,15 @@ public function GetTotalMembers() { ]; } + /** + * countByDate. + * + * @param mixed[] $arr + * @param mixed $date + * @param mixed $group + * + * @return mixed[] + */ private function countByDate($arr, $date, $group) { if (is_null($date)) { return $arr; @@ -375,15 +447,19 @@ private function countByDate($arr, $date, $group) { switch ($group) { case 'day': $grouping = $grouping->format('Y-m-d'); + break; case 'month': $grouping = $grouping->format('Y-m'); + break; case 'year': $grouping = $grouping->format('Y'); + break; default: $grouping = 'all'; + break; } diff --git a/app/services/PaymentService.php b/packages/backend-php/app/services/PaymentService.php similarity index 71% rename from app/services/PaymentService.php rename to packages/backend-php/app/services/PaymentService.php index 9e4424a15..e3c6f9b8e 100644 --- a/app/services/PaymentService.php +++ b/packages/backend-php/app/services/PaymentService.php @@ -5,7 +5,7 @@ use app\contracts\IPaymentService1; use app\domain\Payment; use app\processors\PaymentProcessor; -use Aws\CloudFront\Exception\Exception; +use vhs\domain\Domain; use vhs\domain\Filter; use vhs\loggers\StringLogger; use vhs\security\CurrentUser; @@ -16,7 +16,7 @@ class PaymentService extends Service implements IPaymentService1 { /** * @permission administrator|user * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -27,8 +27,8 @@ public function CountPayments($filters) { /** * @permission administrator|user * - * @param $userid - * @param $filters + * @param int $userid + * @param string|\vhs\domain\Filter|null $filters * * @return int */ @@ -41,11 +41,12 @@ public function CountUserPayments($userid, $filters) { /** * @permission administrator|user * - * @param $id + * @param int $id * * @return mixed */ public function GetPayment($id) { + /** @var \app\domain\Payment */ $payment = Payment::find($id); if (is_null($payment)) { @@ -62,11 +63,11 @@ public function GetPayment($id) { /** * @permission administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -77,12 +78,12 @@ public function ListPayments($page, $size, $columns, $order, $filters) { /** * @permission administrator|user * - * @param $userid - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -95,11 +96,12 @@ public function ListUserPayments($userid, $page, $size, $columns, $order, $filte /** * @permission administrator * - * @param $paymentid + * @param int $paymentid * * @return mixed */ public function ReplayPaymentProcessing($paymentid) { + /** @var StringLogger */ $log = new StringLogger(); $log->log('Attempting a reply of payment id: ' . $paymentid); @@ -115,17 +117,25 @@ public function ReplayPaymentProcessing($paymentid) { $log->log('Replay complete.'); + // @phpstan-ignore method.notFound return $log->fullText(); } + /** + * AddUserIDOrEMailToFilters. + * + * @param int $userid + * @param Filter|string $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Filter + */ private function AddUserIDOrEMailToFilters($userid, $filters) { $userService = new UserService(); $user = $userService->GetUser($userid); - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } + Domain::coerceFilters($filters); if (is_null($user)) { throw new UnauthorizedException('User not found or you do not have access'); diff --git a/app/services/PinService.php b/packages/backend-php/app/services/PinService.php similarity index 68% rename from app/services/PinService.php rename to packages/backend-php/app/services/PinService.php index 80c029896..6a2cba2e7 100644 --- a/app/services/PinService.php +++ b/packages/backend-php/app/services/PinService.php @@ -21,6 +21,7 @@ use vhs\security\exceptions\UnauthorizedException; use vhs\services\Service; +/** @typescript */ class PinService extends Service implements IPinService1 { /** * @permission door @@ -36,7 +37,9 @@ public function AccessInstructions() { * * @permission administrator|user * - * @param $userid + * @param int $userid + * + * @throws \vhs\security\exceptions\UnauthorizedException * * @return mixed */ @@ -48,7 +51,11 @@ public function GeneratePin($userid) { $pin = $this->GetUserPin($userid); if (is_null($pin)) { - $nextpinid = Database::scalar(Query::Select(SettingsSchema::Table(), new Columns(SettingsSchema::Columns()->nextpinid))); + $nextpinid = Database::scalar( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Query::Select(SettingsSchema::Table(), new Columns(SettingsSchema::Columns()->nextpinid)) + ); $key = new Key(); $key->userid = $userid; @@ -60,6 +67,8 @@ public function GeneratePin($userid) { $priv = Privilege::findByCode('inherit'); if (!is_null($priv)) { + // TODO fix typing + /** @disregard P1006 override */ $pin->privileges->add($priv); } } @@ -77,16 +86,23 @@ public function GeneratePin($userid) { /** * @permission gen-temp-pin|administrator * - * @param $expires - * @param $privileges - * @param $notes + * @param string $expires + * @param string $privileges + * @param string $notes * * @return mixed */ public function GenerateTemporaryPin($expires, $privileges, $notes) { $userid = CurrentUser::getIdentity(); - $nextpinid = Database::scalar(Query::Select(SettingsSchema::Table(), new Columns(SettingsSchema::Columns()->nextpinid))); + $nextpinid = Database::scalar( + Query::Select( + SettingsSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + new Columns(SettingsSchema::Columns()->nextpinid) + ) + ); $pin = new Key(); $pin->userid = $userid; @@ -95,17 +111,15 @@ public function GenerateTemporaryPin($expires, $privileges, $notes) { $pin->key = sprintf('%04s', $nextpinid) . '|' . sprintf('%04s', rand(0, 9999)); $pin->notes = $notes; - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = explode(',', $privileges); - } + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; $privs = Privilege::findByCodes(...$privArray); if (!is_null($privs) && is_array($privs)) { foreach ($privs as $priv) { if (CurrentUser::hasAllPermissions($priv->code)) { + // TODO fix typing + /** @disregard P1006 override */ $pin->privileges->add($priv); } } @@ -119,7 +133,9 @@ public function GenerateTemporaryPin($expires, $privileges, $notes) { /** * @permission administrator|user * - * @param $userid + * @param int $userid + * + * @throws \vhs\security\exceptions\UnauthorizedException * * @return mixed */ @@ -128,7 +144,16 @@ public function GetUserPin($userid) { throw new UnauthorizedException(); } - $keys = Key::where(Where::_And(Where::Equal(Key::Schema()->Columns()->type, 'pin'), Where::Equal(Key::Schema()->Columns()->userid, $userid))); + $keys = Key::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Key::Schema()->Columns()->type, 'pin'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Key::Schema()->Columns()->userid, $userid) + ) + ); if (count($keys) >= 1) { return $keys[0]; @@ -140,12 +165,15 @@ public function GetUserPin($userid) { /** * @permission administrator|user * - * @param $keyid - * @param $pin + * @param int $keyid + * @param string $pin * - * @return mixed + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return void */ public function UpdatePin($keyid, $pin) { + /** @var \app\domain\Key */ $key = Key::find($keyid); if (!CurrentUser::hasAnyPermissions('administrator') && $key->userid != CurrentUser::getIdentity()) { @@ -164,7 +192,10 @@ public function UpdatePin($keyid, $pin) { * * @permission administrator|user * - * @param $pin + * @param int $userid + * @param string $pin + * + * @throws \vhs\security\exceptions\UnauthorizedException * * @return mixed */ diff --git a/app/services/PreferenceService.php b/packages/backend-php/app/services/PreferenceService.php similarity index 75% rename from app/services/PreferenceService.php rename to packages/backend-php/app/services/PreferenceService.php index c85ba5fe5..befda953c 100644 --- a/app/services/PreferenceService.php +++ b/packages/backend-php/app/services/PreferenceService.php @@ -15,13 +15,14 @@ use vhs\security\CurrentUser; use vhs\services\Service; +/** @typescript */ class PreferenceService extends Service implements IPreferenceService1 { /** * @permission administrator * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * - * @return array + * @return int */ public function CountSystemPreferences($filters) { return SystemPreference::count($filters); @@ -30,11 +31,12 @@ public function CountSystemPreferences($filters) { /** * @permission administrator * - * @param $key + * @param string $key * * @return mixed */ public function DeleteSystemPreference($key) { + /** @var \app\domain\SystemPreference[] */ $prefs = SystemPreference::findByKey($key); if (is_null($prefs) || count($prefs) <= 0) { @@ -58,34 +60,38 @@ public function GetAllSystemPreferences() { /** * @permission administrator * - * @param $key + * @param int $id * * @return mixed */ public function GetSystemPreference($id) { + /** @var \app\domain\SystemPreference */ return SystemPreference::find($id); } /** * @permission administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param mixed $columns + * @param mixed $order + * @param string|\vhs\domain\Filter|null $filters * - * @return array + * @return \app\domain\SystemPreference[] */ public function ListSystemPreferences($page, $size, $columns, $order, $filters) { + /** @var \app\domain\SystemPreference[] */ return SystemPreference::page($page, $size, $columns, $order, $filters); } /** * @permission administrator * - * @param $key - * @param $value + * @param string $key + * @param string $value + * @param bool $enabled + * @param string $notes * * @return mixed */ @@ -113,23 +119,20 @@ public function PutSystemPreference($key, $value, $enabled, $notes) { /** * @permission administrator * - * @param $id - * @param $privileges + * @param int $id + * @param string $privileges * * @return mixed */ public function PutSystemPreferencePrivileges($id, $privileges) { + /** @var \app\domain\SystemPreference */ $pref = SystemPreference::find($id); if (is_null($pref)) { return; } - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = explode(',', $privileges); - } + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; $privs = Privilege::findByCodes(...$privArray); @@ -147,11 +150,12 @@ public function PutSystemPreferencePrivileges($id, $privileges) { /** * @permission anonymous * - * @param $key + * @param string $key * * @return mixed */ public function SystemPreference($key) { + /** @var \app\domain\SystemPreference[] */ $prefs = SystemPreference::findByKey($key, function ($privileges) { $codes = []; foreach ($privileges->all() as $priv) { @@ -171,13 +175,16 @@ public function SystemPreference($key) { /** * @permission administrator * - * @param $id - * @param $key - * @param $value + * @param int $id + * @param string $key + * @param string $value + * @param bool $enabled + * @param string $notes * * @return mixed */ public function UpdateSystemPreference($id, $key, $value, $enabled, $notes) { + /** @var \app\domain\SystemPreference */ $pref = SystemPreference::find($id); if (is_null($pref)) { @@ -197,12 +204,13 @@ public function UpdateSystemPreference($id, $key, $value, $enabled, $notes) { /** * @permission administrator * - * @param $key - * @param $enabled + * @param string $key + * @param bool $enabled * * @return mixed */ public function UpdateSystemPreferenceEnabled($key, $enabled) { + /** @var \app\domain\SystemPreference[] */ $prefs = SystemPreference::findByKey($key); $pref = null; diff --git a/app/services/PrivilegeService.php b/packages/backend-php/app/services/PrivilegeService.php similarity index 78% rename from app/services/PrivilegeService.php rename to packages/backend-php/app/services/PrivilegeService.php index f491ddb07..df246ea59 100644 --- a/app/services/PrivilegeService.php +++ b/packages/backend-php/app/services/PrivilegeService.php @@ -12,8 +12,6 @@ use app\contracts\IPrivilegeService1; use app\domain\Privilege; use app\exceptions\InvalidInputException; -use app\exceptions\MemberCardException; -use vhs\security\exceptions\UnauthorizedException; use vhs\services\endpoints\Endpoint; use vhs\services\Service; use vhs\services\ServiceRegistry; @@ -22,7 +20,7 @@ class PrivilegeService extends Service implements IPrivilegeService1 { /** * @permission administrator|user|grants * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -33,11 +31,13 @@ public function CountPrivileges($filters) { /** * @permission administrator * - * @param $name - * @param $code - * @param $description - * @param $icon - * @param $enabled + * @param string $name + * @param string $code + * @param string $description + * @param string $icon + * @param bool $enabled + * + * @throws \app\exceptions\InvalidInputException * * @return mixed */ @@ -64,9 +64,9 @@ public function CreatePrivilege($name, $code, $description, $icon, $enabled) { /** * @permission administrator * - * @param $id + * @param int $id * - * @return mixed + * @return void */ public function DeletePrivilege($id) { $priv = Privilege::find($id); @@ -77,9 +77,10 @@ public function DeletePrivilege($id) { /** * @permission administrator|user|grants * - * @return mixed + * @return \app\domain\Privilege[] */ public function GetAllPrivileges() { + /** @var \app\domain\Privilege[] */ return Privilege::findAll(); } @@ -116,18 +117,19 @@ public function GetAllSystemPermissions() { /** * @permission user * - * @param $id + * @param int $id * - * @return mixed + * @return \app\domain\Privilege */ public function GetPrivilege($id) { + /** @var \app\domain\Privilege */ return Privilege::find($id); } /** * @permission administrator|user * - * @param $userid + * @param int $userid * * @return mixed */ @@ -153,11 +155,11 @@ public function GetUserPrivileges($userid) { /** * @permission administrator|user|grants * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -168,12 +170,13 @@ public function ListPrivileges($page, $size, $columns, $order, $filters) { /** * @permission administrator * - * @param $id - * @param $description + * @param int $id + * @param string $description * * @return mixed */ public function UpdatePrivilegeDescription($id, $description) { + /** @var \app\domain\Privilege */ $priv = Privilege::find($id); $priv->description = $description; @@ -184,12 +187,13 @@ public function UpdatePrivilegeDescription($id, $description) { /** * @permission administrator * - * @param $id - * @param $enabled + * @param int $id + * @param bool $enabled * * @return mixed */ public function UpdatePrivilegeEnabled($id, $enabled) { + /** @var \app\domain\Privilege */ $priv = Privilege::find($id); $priv->enabled = $enabled; @@ -200,12 +204,13 @@ public function UpdatePrivilegeEnabled($id, $enabled) { /** * @permission administrator * - * @param $id - * @param $icon + * @param int $id + * @param string $icon * * @return mixed */ public function UpdatePrivilegeIcon($id, $icon) { + /** @var \app\domain\Privilege */ $priv = Privilege::find($id); $priv->icon = $icon; @@ -216,12 +221,13 @@ public function UpdatePrivilegeIcon($id, $icon) { /** * @permission administrator * - * @param $id - * @param $name + * @param int $id + * @param string $name * * @return mixed */ public function UpdatePrivilegeName($id, $name) { + /** @var \app\domain\Privilege */ $priv = Privilege::find($id); $priv->name = $name; diff --git a/app/services/StripeEventService.php b/packages/backend-php/app/services/StripeEventService.php similarity index 73% rename from app/services/StripeEventService.php rename to packages/backend-php/app/services/StripeEventService.php index d3a57ae83..c6224a245 100644 --- a/app/services/StripeEventService.php +++ b/packages/backend-php/app/services/StripeEventService.php @@ -11,11 +11,12 @@ use app\domain\StripeEvent; use vhs\services\Service; +/** @typescript */ class StripeEventService extends Service implements IStripeEventService1 { /** * @permission administrator * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return int */ @@ -26,7 +27,7 @@ public function CountRecords($filters) { /** * @permission administrator * - * @param $ipnId + * @param int $eventId * * @return mixed */ @@ -46,11 +47,11 @@ public function GetAll() { /** * @permission administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ diff --git a/app/services/UserService.php b/packages/backend-php/app/services/UserService.php similarity index 83% rename from app/services/UserService.php rename to packages/backend-php/app/services/UserService.php index de2647224..60cb528f9 100644 --- a/app/services/UserService.php +++ b/packages/backend-php/app/services/UserService.php @@ -15,6 +15,7 @@ use app\domain\PasswordResetRequest; use app\domain\Privilege; use app\domain\User; +use app\dto\UserActiveEnum; use app\exceptions\InvalidInputException; use app\exceptions\InvalidPasswordHashException; use app\exceptions\UserAlreadyExistsException; @@ -23,11 +24,12 @@ use vhs\security\CurrentUser; use vhs\services\Service; +/** @typescript */ class UserService extends Service implements IUserService1 { /** * @permission administrator|grants * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -38,12 +40,15 @@ public function CountUsers($filters) { /** * @permission administrator * - * @param $username - * @param $password - * @param $email - * @param $fname - * @param $lname - * @param $membershipid + * @param string $username + * @param string $password + * @param string $email + * @param string $fname + * @param string $lname + * @param int $membershipid + * + * @throws \app\exceptions\InvalidPasswordHashException + * @throws \app\exceptions\UserAlreadyExistsException * * @return mixed */ @@ -68,7 +73,7 @@ public function Create($username, $password, $email, $fname, $lname, $membership $user->stripe_email = $email; $user->fname = $fname; $user->lname = $lname; - $user->active = 't'; + $user->active = UserActiveEnum::PENDING->value; $user->token = bin2hex(openssl_random_pseudo_bytes(8)); $user->save(); @@ -98,7 +103,7 @@ public function Create($username, $password, $email, $fname, $lname, $membership /** * @permission grants * - * @param $userid + * @param int $userid * * @return mixed */ @@ -122,7 +127,7 @@ public function GetGrantUserPrivileges($userid) { /** * @permission user|administrator * - * @param $userid + * @param int $userid * * @return mixed */ @@ -153,7 +158,7 @@ public function GetStatuses() { /** * @permission administrator|user * - * @param $userid + * @param int $userid * * @return mixed */ @@ -177,8 +182,8 @@ public function GetUsers() { /** * @permission grants * - * @param $userid - * @param $privilege + * @param int $userid + * @param string $privilege * * @return mixed */ @@ -200,12 +205,16 @@ public function GrantPrivilege($userid, $privilege) { return; } + // TODO fix typing + /** @disregard P1006 override */ foreach ($user->privileges->all() as $p) { if ($p->code == $priv->code) { return; } } + // TODO fix typing + /** @disregard P1006 override */ $user->privileges->add($priv); $user->save(); } @@ -213,11 +222,11 @@ public function GrantPrivilege($userid, $privilege) { /** * @permission administrator|grants * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -228,25 +237,29 @@ public function ListUsers($page, $size, $columns, $order, $filters) { /** * @permission administrator * - * @param $userid - * @param $privileges + * @param int $userid + * @param string $privileges + * + * @return void */ public function PutUserPrivileges($userid, $privileges) { $user = User::find($userid); - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = explode(',', $privileges); - } + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; $privs = Privilege::findByCodes(...$privArray); + // TODO fix typing + /** @disregard P1006 override */ foreach ($user->privileges->all() as $priv) { + // TODO fix typing + /** @disregard P1006 override */ $user->privileges->remove($priv); } foreach ($privs as $priv) { + // TODO fix typing + /** @disregard P1006 override */ $user->privileges->add($priv); } @@ -256,11 +269,14 @@ public function PutUserPrivileges($userid, $privileges) { /** * @permission anonymous * - * @param $username - * @param $password - * @param $email - * @param $fname - * @param $lname + * @param string $username + * @param string $password + * @param string $email + * @param string $fname + * @param string $lname + * + * @throws \app\exceptions\InvalidPasswordHashException + * @throws \app\exceptions\UserAlreadyExistsException * * @return mixed */ @@ -282,7 +298,7 @@ public function Register($username, $password, $email, $fname, $lname) { $user->email = $email; $user->fname = $fname; $user->lname = $lname; - $user->active = 't'; + $user->active = UserActiveEnum::PENDING->value; $user->token = bin2hex(openssl_random_pseudo_bytes(8)); $user->save(); @@ -306,7 +322,7 @@ public function Register($username, $password, $email, $fname, $lname) { /** * @permission anonymous * - * @param $email + * @param string $email * * @return mixed */ @@ -341,7 +357,7 @@ public function RequestPasswordReset($email) { /** * @permission user * - * @param $email + * @param string $email * * @return mixed */ @@ -356,14 +372,13 @@ public function RequestSlackInvite($email) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($body)); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($ch, CURLOPT_FORBID_REUSE, 1); - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Connection: Close']); - + curl_setopt($ch, CURLOPT_FORBID_REUSE, true); + $error = null; $response = curl_exec($ch); @@ -407,8 +422,10 @@ public function RequestSlackInvite($email) { /** * @permission anonymous * - * @param $token - * @param $password + * @param string $token + * @param string $password + * + * @throws \app\exceptions\InvalidPasswordHashException * * @return mixed */ @@ -467,10 +484,10 @@ public function ResetPassword($token, $password) { /** * @permission grants * - * @param $userid - * @param $privilege + * @param int $userid + * @param string $privilege * - * @return mixed + * @return void */ public function RevokePrivilege($userid, $privilege) { if (!CurrentUser::canGrantAllPermissions($privilege)) { @@ -491,6 +508,8 @@ public function RevokePrivilege($userid, $privilege) { $remove = null; + // TODO fix typing + /** @disregard P1006 override */ foreach ($user->privileges->all() as $p) { if ($p->code == $priv->code) { $remove = $p; @@ -498,6 +517,8 @@ public function RevokePrivilege($userid, $privilege) { } if (!is_null($remove)) { + // TODO fix typing + /** @disregard P1006 override */ $user->privileges->remove($remove); $user->save(); } @@ -506,10 +527,10 @@ public function RevokePrivilege($userid, $privilege) { /** * @permission administrator * - * @param $userid - * @param $cash + * @param int $userid + * @param bool $cash * - * @return mixed + * @return void */ public function UpdateCash($userid, $cash) { $user = User::find($userid); @@ -526,10 +547,10 @@ public function UpdateCash($userid, $cash) { /** * @permission administrator|full-profile * - * @param $userid - * @param $email + * @param int $userid + * @param string $email * - * @return mixed + * @return void */ public function UpdateEmail($userid, $email) { $user = $this->GetUser($userid); @@ -546,8 +567,8 @@ public function UpdateEmail($userid, $email) { /** * @permission administrator * - * @param $userid - * @param $date + * @param int $userid + * @param string $date * * @return mixed */ @@ -566,14 +587,18 @@ public function UpdateExpiry($userid, $date) { /** * @permission administrator * - * @param $userid - * @param $membershipid + * @param int $userid + * @param int $membershipid + * + * @throws \app\exceptions\InvalidInputException * * @return mixed */ public function UpdateMembership($userid, $membershipid) { + /** @var User|null */ $user = User::find($userid); + /** @var Membership|null */ $membership = Membership::find($membershipid); if (is_null($user) || is_null($membership)) { @@ -588,9 +613,9 @@ public function UpdateMembership($userid, $membershipid) { /** * @permission administrator|full-profile * - * @param $userid - * @param $fname - * @param $lname + * @param int $userid + * @param string $fname + * @param string $lname * * @return mixed */ @@ -610,8 +635,10 @@ public function UpdateName($userid, $fname, $lname) { /** * @permission administrator|user * - * @param $userid - * @param $subscribe + * @param int $userid + * @param bool $subscribe + * + * @return void */ public function UpdateNewsletter($userid, $subscribe) { $user = $this->GetUser($userid); @@ -628,8 +655,10 @@ public function UpdateNewsletter($userid, $subscribe) { /** * @permission administrator|user * - * @param $userid - * @param $password + * @param int $userid + * @param string $password + * + * @throws \app\exceptions\InvalidPasswordHashException */ public function UpdatePassword($userid, $password) { $user = $this->GetUser($userid); @@ -652,8 +681,8 @@ public function UpdatePassword($userid, $password) { /** * @permission administrator|full-profile * - * @param $userid - * @param $email + * @param int $userid + * @param string $email * * @return void */ @@ -672,8 +701,8 @@ public function UpdatePaymentEmail($userid, $email) { /** * @permission administrator * - * @param $userid - * @param $status + * @param int $userid + * @param string $status * * @return mixed */ @@ -689,17 +718,21 @@ public function UpdateStatus($userid, $status) { case 'y': case 'true': $status = 'y'; + break; case 'pending': case 't': $status = 't'; + break; case 'banned': case 'b': $status = 'b'; + break; default: $status = 'n'; + break; } @@ -711,8 +744,8 @@ public function UpdateStatus($userid, $status) { /** * @permission administrator|full-profile * - * @param $userid - * @param $email + * @param int $userid + * @param string $email * * @return void */ @@ -731,8 +764,10 @@ public function UpdateStripeEmail($userid, $email) { /** * @permission administrator|user * - * @param $userid - * @param $username + * @param int $userid + * @param string $username + * + * @return void */ public function UpdateUsername($userid, $username) { $user = $this->GetUser($userid); @@ -746,6 +781,11 @@ public function UpdateUsername($userid, $username) { $user->save(); } + /** + * AllowedColumns. + * + * @return string[]|null + */ protected function AllowedColumns() { if (CurrentUser::hasAnyPermissions('grants') && !CurrentUser::hasAnyPermissions('administrator')) { return ['id', 'username', 'fname', 'lname', 'email']; diff --git a/app/services/WebHookService.php b/packages/backend-php/app/services/WebHookService.php similarity index 73% rename from app/services/WebHookService.php rename to packages/backend-php/app/services/WebHookService.php index 7ddef791b..440ffccac 100644 --- a/app/services/WebHookService.php +++ b/packages/backend-php/app/services/WebHookService.php @@ -12,17 +12,18 @@ use app\contracts\IWebHookService1; use app\domain\Privilege; use app\domain\WebHook; -use app\exceptions\MemberCardException; +use vhs\domain\Domain; use vhs\domain\Filter; use vhs\security\CurrentUser; use vhs\security\exceptions\UnauthorizedException; use vhs\services\Service; +/** @typescript */ class WebHookService extends Service implements IWebHookService1 { /** * @permission administrator|webhook * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return int */ @@ -33,8 +34,8 @@ public function CountHooks($filters) { /** * @permission administrator|user * - * @param $userid - * @param $filters + * @param int $userid + * @param string|\vhs\domain\Filter|null $filters * * @return int */ @@ -47,18 +48,18 @@ public function CountUserHooks($userid, $filters) { /** * @permission user * - * @param $name - * @param $description - * @param $enabled - * @param $url - * @param $translation - * @param $headers - * @param $method - * @param $eventid + * @param string $name + * @param string $description + * @param bool $enabled + * @param string $url + * @param string $translation + * @param string $headers + * @param string $method + * @param int $eventid * - * @return mixed + * @throws \vhs\security\exceptions\UnauthorizedException * - * @throws UnauthorizedException + * @return mixed */ public function CreateHook($name, $description, $enabled, $url, $translation, $headers, $method, $eventid) { $event = (new EventService($this->context))->GetEvent($eventid); @@ -68,7 +69,7 @@ public function CreateHook($name, $description, $enabled, $url, $translation, $h array_push($codes, $priv->code); } - if (!CurrentUser::hasAllPermissions('administrator') && (count($codes) == 0 || !CurrentUser::hasAllPermissions($codes))) { + if (!CurrentUser::hasAllPermissions('administrator') && (count($codes) == 0 || !CurrentUser::hasAllPermissions(...$codes))) { throw new UnauthorizedException('Insufficient privileges to subscribe to event'); } @@ -90,7 +91,7 @@ public function CreateHook($name, $description, $enabled, $url, $translation, $h /** * @permission administrator|user * - * @param $id + * @param int $id * * @return mixed */ @@ -107,8 +108,8 @@ public function DeleteHook($id) { /** * @permission administrator|user * - * @param $id - * @param $enabled + * @param int $id + * @param bool $enabled * * @return mixed */ @@ -136,11 +137,12 @@ public function GetAllHooks() { /** * @permission user|administrator * - * @param $id + * @param int $id * - * @return mixed + * @return \app\domain\WebHook|null */ public function GetHook($id) { + /** @var \app\domain\WebHook */ $hook = WebHook::find($id); if (is_null($hook)) { @@ -157,8 +159,8 @@ public function GetHook($id) { /** * @permission webhook|administrator * - * @param $domain - * @param $event + * @param string $domain + * @param string $event * * @return mixed */ @@ -169,11 +171,11 @@ public function GetHooks($domain, $event) { /** * @permission administrator|webhook * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -184,16 +186,14 @@ public function ListHooks($page, $size, $columns, $order, $filters) { /** * @permission administrator|user * - * @param $userid - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed - * - * @throws \Exception */ public function ListUserHooks($userid, $page, $size, $columns, $order, $filters) { $filters = $this->AddUserIDToFilters($userid, $filters); @@ -210,8 +210,8 @@ public function ListUserHooks($userid, $page, $size, $columns, $order, $filters) /** * @permission administrator|user * - * @param $id - * @param $privileges + * @param int $id + * @param string $privileges * * @return mixed */ @@ -222,11 +222,7 @@ public function PutHookPrivileges($id, $privileges) { return; } - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = explode(',', $privileges); - } + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; $privs = Privilege::findByCodes(...$privArray); @@ -246,15 +242,15 @@ public function PutHookPrivileges($id, $privileges) { /** * @permission administrator|user * - * @param $id - * @param $name - * @param $description - * @param $enabled - * @param $url - * @param $translation - * @param $headers - * @param $method - * @param $eventid + * @param int $id + * @param string $name + * @param string $description + * @param bool $enabled + * @param string $url + * @param string $translation + * @param string $headers + * @param string $method + * @param int $eventid * * @return mixed */ @@ -279,14 +275,21 @@ public function UpdateHook($id, $name, $description, $enabled, $url, $translatio $hook->save(); } + /** + * AddUserIDToFilters. + * + * @param int $userid + * @param Filter|string $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Filter + */ private function AddUserIDToFilters($userid, $filters) { $userService = new UserService(); $user = $userService->GetUser($userid); - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } + Domain::coerceFilters($filters); if (is_null($user)) { throw new UnauthorizedException('User not found or you do not have access'); diff --git a/packages/backend-php/app/utils/AuthCheckResult.php b/packages/backend-php/app/utils/AuthCheckResult.php new file mode 100644 index 000000000..8da2479c3 --- /dev/null +++ b/packages/backend-php/app/utils/AuthCheckResult.php @@ -0,0 +1,36 @@ + */ + private array $store = []; + + public function __get(string $name): mixed { + return $this->store[$name]; + } + + public function __isset(string $name): bool { + return isset($this->store[$name]); + } + + public function __serialize(): array { + return ['valid' => $this->valid, 'type' => $this->type, 'privileges' => $this->privileges, ...$this->store]; + } + + public function __set(string $name, mixed $value): void { + $this->store[$name] = $value; + } + + public function __unset(string $name): void { + unset($this->store[$name]); + } +} diff --git a/packages/backend-php/app/utils/DTO.php b/packages/backend-php/app/utils/DTO.php new file mode 100644 index 000000000..ba02f15d4 --- /dev/null +++ b/packages/backend-php/app/utils/DTO.php @@ -0,0 +1,36 @@ + $data + */ +class DTO extends stdClass { + /** + * __construct. + * + * @param array|object $data + * + * @return void + */ + public function __construct($data) { + if ( + (gettype($data) !== 'array' && gettype($data) !== 'object') || + (gettype($data) === 'array' && empty($data)) || + (gettype($data) === 'object' && count(get_object_vars($data)) === 0) + ) { + throw new Exception('Missing DTO construction argument!'); + } + + foreach ($data as $k => $v) { + $this->{$k} = $v; + } + } +} diff --git a/packages/backend-php/app/utils/EnumMapper.php b/packages/backend-php/app/utils/EnumMapper.php new file mode 100644 index 000000000..4eb7ad7a5 --- /dev/null +++ b/packages/backend-php/app/utils/EnumMapper.php @@ -0,0 +1,23 @@ + $e->name, array_values($cases)); + + if (in_array($needle, $vals)) { + return array_search($needle, $cases); + } + + return null; + } +} diff --git a/packages/backend-php/app/utils/IDTO.php b/packages/backend-php/app/utils/IDTO.php new file mode 100644 index 000000000..3a6568524 --- /dev/null +++ b/packages/backend-php/app/utils/IDTO.php @@ -0,0 +1,6 @@ +\n ]/', trim($result)); + + foreach ($matches as $match) { + if (strrpos($match, '\\') !== false) { + $result = str_replace($match, PHP2TS::getBaseContractInterface($match), subject: $result); + } + } + + return $result; + } + + /** + * convertDocComment. + * + * @param string $docComment + * + * @return string + */ + public static function convertDocComment($docComment) { + $factory = DocBlockFactory::createInstance(); + $docblock = $factory->create($docComment); + + $reconstructed = []; + + if (strlen(trim(string: $docblock->getSummary())) > 0) { + $reconstructed[] = $docblock->getSummary(); + } + + if (strlen(trim(string: $docblock->getDescription())) > 0) { + if (!empty($reconstructed)) { + $reconstructed[] = ''; + } + array_push($reconstructed, ...explode("\n", $docblock->getDescription())); + } + + $lastTag = ''; + + /** @var \phpDocumentor\Reflection\DocBlock\Tags\Param[]|\phpDocumentor\Reflection\DocBlock\Tags\Property[]|\phpDocumentor\Reflection\DocBlock\Tags\TagWithType[] */ + $tags = $docblock->getTags(); + + $longestType = max(array_map(fn ($tag): int => $tag->getName() === 'param' ? strlen(PHP2TS::convertDataType($tag->getType())) + 2 : 0, $tags)); + $longestVarName = max(array_map(fn ($tag): int => $tag->getName() === 'param' ? strlen($tag->getVariableName()) : 0, $tags)); + + foreach ($tags as $tag) { + if ($tag->getName() !== $lastTag) { + if (!empty($reconstructed)) { + $reconstructed[] = ''; + } + $lastTag = $tag->getName(); + } + + $tagRow = [sprintf('@%s', $tag->getName())]; + + switch ($tag->getName()) { + case 'param': + /** @disregard P1013 manually checking */ + $tagRow[] = str_pad(sprintf('{%s}', PHP2TS::convertDataType($tag->getType())), $longestType, ' '); + /** @disregard P1013 manually checking */ + $tagRow[] = str_pad($tag->getVariableName(), $longestVarName, ' '); + /** @disregard P1013 manually checking */ + $tagRow[] = $tag->getDescription(); + + break; + case 'throws': + /** @disregard P1013 manually checking */ + $tagRow[] = str_pad(sprintf('{%s}', PHP2TS::convertDataType($tag->getType())), $longestVarName + 2, ' '); + /** @disregard P1013 manually checking */ + $tagRow[] = $tag->getDescription(); + + break; + case 'return': + $tagRow[0] = '@returns'; + /** @disregard P1013 manually checking */ + $tagRow[] = str_pad(sprintf('{%s}', PHP2TS::convertDataType($tag->getType())), $longestVarName + 1, ' '); + /** @disregard P1013 manually checking */ + $tagRow[] = $tag->getDescription(); + + break; + default: + /** @disregard P1013 manually checking */ + $tagRow[] = $tag->getDescription(); + + break; + } + + $reconstructed[] = implode(' ', $tagRow); + } + + $output = []; + $output[] = ' /**'; + + array_push($output, ...array_map(fn ($row): string => sprintf(' * %s', $row), $reconstructed)); + + $output[] = ' */'; + + return implode("\n", $output); + } + + /** + * generateContractMethodArgs. + * + * @param mixed $contractMethod + * + * @return string + */ + public static function generateContractMethodArgs($contractMethod): string { + $params = $contractMethod->getParameters(); + + return join( + ', ', + array_map( + fn ($param): string => sprintf( + '%s: %s', + $param->getName(), + trim(str_replace('$', '', PHP2TS::getDocCommentParam($contractMethod, $param->getName()))) + ), + $params + ) + ); + } + + /** + * generateContractMethodParams. + * + * @param mixed $contractMethod + * + * @return string + */ + public static function generateContractMethodParams($contractMethod): string { + $params = $contractMethod->getParameters(); + + return join(', ', array_map(fn ($param): string => sprintf('%s', $param->getName()), $params)); + } + + /** + * generateContractMethodReturnType. + * + * @param mixed $contractMethod + * + * @return string + */ + public static function generateContractMethodReturnType($contractMethod): string { + $docComment = $contractMethod->getDocComment(); + + $factory = DocBlockFactory::createInstance(); + $docblock = $factory->create($docComment); + + $tags = $docblock->getTags(); + + $tag = null; + + foreach ($tags as $tag) { + if ($tag->getName() === 'return') { + /** @disregard P1013 manually checking */ + return PHP2TS::convertDataType($tag->getType()); + } + } + + throw new \Exception( + sprintf('Missing return statement for: %s->%s', $contractMethod->getDeclaringClass()->getName(), $contractMethod->getName()) + ); + } + + /** + * getBaseContractInterface. + * + * @param string $name + * + * @return string + */ + public static function getBaseContractInterface(string $name): string { + return substr($name, strrpos($name, '\\') + 1); + } + + /** + * getDocCommentParam. + * + * @param mixed $contractMethod + * @param string $param + * + * @return mixed + */ + public static function getDocCommentParam($contractMethod, string $param): mixed { + $docComment = $contractMethod->getDocComment(); + + $factory = DocBlockFactory::createInstance(); + $docblock = $factory->create($docComment); + + $tags = $docblock->getTags(); + + $tag = null; + + foreach ($tags as $tag) { + /** @disregard P1013 manually checking */ + if ($tag->getName() === 'param' && $tag->getVariableName() === $param) { + /** @disregard P1013 manually checking */ + return PHP2TS::convertDataType($tag->getType()); + } + } + + throw new \Exception( + sprintf( + 'Missing @param doc comment for param %s of %s->%s', + $param, + $contractMethod->getDeclaringClass()->getName(), + $contractMethod->getName() + ) + ); + } +} diff --git a/packages/backend-php/composer.json b/packages/backend-php/composer.json new file mode 100644 index 000000000..8ed537f42 --- /dev/null +++ b/packages/backend-php/composer.json @@ -0,0 +1,24 @@ +{ + "require": { + "aws/aws-sdk-php": "3.342.2", + "nicmart/string-template": "0.1.3", + "league/oauth2-client": "2.8.1", + "stripe/stripe-php": "7.128.0", + "league/oauth2-github": "3.1.1", + "league/oauth2-google": "4.0.1" + }, + "require-dev": { + "phpunit/phpunit": "11.5.3", + "friendsofphp/php-cs-fixer": "v3.68.0", + "spatie/typescript-transformer": "^2.4", + "phpdocumentor/shim": "^3.7", + "vimeo/psalm": "^6.5", + "phpstan/phpstan": "^2.1", + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "config": { + "allow-plugins": { + "phpdocumentor/shim": true + } + } +} diff --git a/composer.lock b/packages/backend-php/composer.lock similarity index 64% rename from composer.lock rename to packages/backend-php/composer.lock index 140b2b877..b5777bd7a 100644 --- a/composer.lock +++ b/packages/backend-php/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bdb5c348f93facc5eb35d2cb20f96335", + "content-hash": "bf2a77ac2ab0ff4d2209dbfc40887211", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.336.8", + "version": "3.342.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "933da0d1b9b1ac9b37d5e32e127d4581b1aabaf6" + "reference": "ef66e0fdba9e7f786a7b1e522f847d76d0320e89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/933da0d1b9b1ac9b37d5e32e127d4581b1aabaf6", - "reference": "933da0d1b9b1ac9b37d5e32e127d4581b1aabaf6", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ef66e0fdba9e7f786a7b1e522f847d76d0320e89", + "reference": "ef66e0fdba9e7f786a7b1e522f847d76d0320e89", "shasum": "" }, "require": { @@ -79,31 +79,30 @@ "ext-json": "*", "ext-pcre": "*", "ext-simplexml": "*", - "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", - "guzzlehttp/promises": "^1.4.0 || ^2.0", - "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", - "mtdowling/jmespath.php": "^2.6", - "php": ">=7.2.5", - "psr/http-message": "^1.0 || ^2.0" + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/promises": "^2.0", + "guzzlehttp/psr7": "^2.4.5", + "mtdowling/jmespath.php": "^2.8.0", + "php": ">=8.1", + "psr/http-message": "^2.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", "aws/aws-php-sns-message-validator": "~1.0", "behat/behat": "~3.0", - "composer/composer": "^1.10.22", + "composer/composer": "^2.7.8", "dms/phpunit-arraysubset-asserts": "^0.4.0", "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", "ext-pcntl": "*", "ext-sockets": "*", - "nette/neon": "^2.3", - "paragonie/random_compat": ">= 2", "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", - "psr/cache": "^1.0 || ^2.0 || ^3.0", - "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", - "sebastian/comparator": "^1.2.3 || ^4.0", - "yoast/phpunit-polyfills": "^1.0" + "psr/cache": "^2.0 || ^3.0", + "psr/simple-cache": "^2.0 || ^3.0", + "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", + "symfony/filesystem": "^v6.4.0 || ^v7.1.0", + "yoast/phpunit-polyfills": "^2.0" }, "suggest": { "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", @@ -152,30 +151,30 @@ "sdk" ], "support": { - "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", + "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.336.8" + "source": "https://github.com/aws/aws-sdk-php/tree/3.342.2" }, - "time": "2025-01-03T19:06:11+00:00" + "time": "2025-03-07T19:12:43+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.9.2", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -266,7 +265,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" }, "funding": [ { @@ -282,20 +281,20 @@ "type": "tidelift" } ], - "time": "2024-07-24T11:22:20+00:00" + "time": "2025-08-23T22:36:01+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.4", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + "reference": "481557b130ef3790cf82b713667b43030dc9c957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { @@ -303,7 +302,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "type": "library", "extra": { @@ -349,7 +348,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.4" + "source": "https://github.com/guzzle/promises/tree/2.3.0" }, "funding": [ { @@ -365,20 +364,20 @@ "type": "tidelift" } ], - "time": "2024-10-17T10:06:22+00:00" + "time": "2025-08-22T14:34:08+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.0", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + "reference": "21dc724a0583619cd1652f673303492272778051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", "shasum": "" }, "require": { @@ -394,7 +393,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -465,7 +464,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" + "source": "https://github.com/guzzle/psr7/tree/2.8.0" }, "funding": [ { @@ -481,20 +480,20 @@ "type": "tidelift" } ], - "time": "2024-07-18T11:15:46+00:00" + "time": "2025-08-23T21:21:41+00:00" }, { "name": "league/oauth2-client", - "version": "2.8.0", + "version": "2.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/oauth2-client.git", - "reference": "3d5cf8d0543731dfb725ab30e4d7289891991e13" + "reference": "9df2924ca644736c835fc60466a3a60390d334f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/3d5cf8d0543731dfb725ab30e4d7289891991e13", - "reference": "3d5cf8d0543731dfb725ab30e4d7289891991e13", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/9df2924ca644736c835fc60466a3a60390d334f9", + "reference": "9df2924ca644736c835fc60466a3a60390d334f9", "shasum": "" }, "require": { @@ -544,9 +543,9 @@ ], "support": { "issues": "https://github.com/thephpleague/oauth2-client/issues", - "source": "https://github.com/thephpleague/oauth2-client/tree/2.8.0" + "source": "https://github.com/thephpleague/oauth2-client/tree/2.8.1" }, - "time": "2024-12-11T05:05:52+00:00" + "time": "2025-02-26T04:37:30+00:00" }, { "name": "league/oauth2-github", @@ -779,64 +778,6 @@ }, "time": "2022-10-25T08:03:55+00:00" }, - { - "name": "php-amqplib/php-amqplib", - "version": "v2.5.2", - "source": { - "type": "git", - "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "eb8f94d97c8e79900accf77343dbd7eca7f58506" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/eb8f94d97c8e79900accf77343dbd7eca7f58506", - "reference": "eb8f94d97c8e79900accf77343dbd7eca7f58506", - "shasum": "" - }, - "require": { - "ext-bcmath": "*", - "ext-mbstring": "*", - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*" - }, - "suggest": { - "ext-sockets": "Use AMQPSocketConnection" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.4-dev" - } - }, - "autoload": { - "psr-4": { - "PhpAmqpLib\\": "PhpAmqpLib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-2.1" - ], - "authors": [ - { - "name": "Alvaro Videla" - } - ], - "description": "This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.", - "homepage": "https://github.com/videlalvaro/php-amqplib/", - "keywords": [ - "message", - "queue", - "rabbitmq" - ], - "support": { - "issues": "https://github.com/php-amqplib/php-amqplib/issues", - "source": "https://github.com/php-amqplib/php-amqplib/tree/v2.5.2" - }, - "time": "2015-08-11T12:30:09+00:00" - }, { "name": "psr/http-client", "version": "1.0.3", @@ -1103,16 +1044,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -1125,7 +1066,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -1150,7 +1091,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -1166,23 +1107,24 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -1230,7 +1172,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -1241,41 +1183,51 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" } ], "packages-dev": [ { - "name": "clue/ndjson-react", - "version": "v1.3.0", + "name": "amphp/amp", + "version": "v3.1.1", "source": { "type": "git", - "url": "https://github.com/clue/reactphp-ndjson.git", - "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + "url": "https://github.com/amphp/amp.git", + "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", - "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "url": "https://api.github.com/repos/amphp/amp/zipball/fa0ab33a6f47a82929c38d03ca47ebb71086a93f", + "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f", "shasum": "" }, "require": { - "php": ">=5.3", - "react/stream": "^1.2" + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", - "react/event-loop": "^1.2" + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23.1" }, "type": "library", "autoload": { + "files": [ + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" + ], "psr-4": { - "Clue\\React\\NDJson\\": "src/" + "Amp\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1284,75 +1236,84 @@ ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" } ], - "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", - "homepage": "https://github.com/clue/reactphp-ndjson", + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", "keywords": [ - "NDJSON", - "json", - "jsonlines", - "newline", - "reactphp", - "streaming" + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" ], "support": { - "issues": "https://github.com/clue/reactphp-ndjson/issues", - "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v3.1.1" }, "funding": [ { - "url": "https://clue.engineering/support", - "type": "custom" - }, - { - "url": "https://github.com/clue", + "url": "https://github.com/amphp", "type": "github" } ], - "time": "2022-12-23T10:58:28+00:00" + "time": "2025-08-27T21:42:00+00:00" }, { - "name": "composer/pcre", - "version": "3.3.2", + "name": "amphp/byte-stream", + "version": "v2.1.2", "source": { "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "url": "https://github.com/amphp/byte-stream.git", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<1.11.10" + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" }, "type": "library", - "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] - }, - "branch-alias": { - "dev-main": "3.x-dev" - } - }, "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], "psr-4": { - "Composer\\Pcre\\": "src" + "Amp\\ByteStream\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1361,68 +1322,67 @@ ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" ], "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" }, "funding": [ { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", + "url": "https://github.com/amphp", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2025-03-16T17:10:27+00:00" }, { - "name": "composer/semver", - "version": "3.4.3", + "name": "amphp/cache", + "version": "v2.0.1", "source": { "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "phpstan/phpstan": "^1.11", - "symfony/phpunit-bridge": "^3 || ^7" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, "autoload": { "psr-4": { - "Composer\\Semver\\": "src" + "Amp\\Cache\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1431,77 +1391,71 @@ ], "authors": [ { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" + "name": "Niklas Keller", + "email": "me@kelunik.com" }, { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" } ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", - "keywords": [ - "semantic", - "semver", - "validation", - "versioning" - ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.3" + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" }, "funding": [ { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", + "url": "https://github.com/amphp", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-09-19T14:15:21+00:00" + "time": "2024-04-19T03:38:06+00:00" }, { - "name": "composer/xdebug-handler", - "version": "3.0.5", + "name": "amphp/dns", + "version": "v2.4.0", "source": { "type": "git", - "url": "https://github.com/composer/xdebug-handler.git", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + "url": "https://github.com/amphp/dns.git", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", "shasum": "" }, "require": { - "composer/pcre": "^1 || ^2 || ^3", - "php": "^7.2.5 || ^8.0", - "psr/log": "^1 || ^2 || ^3" + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", + "ext-json": "*", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "phpstan/phpstan": "^1.0", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" }, "type": "library", "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "Composer\\XdebugHandler\\": "src" + "Amp\\Dns\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1510,60 +1464,91 @@ ], "authors": [ { - "name": "John Stevenson", - "email": "john-stevenson@blueyonder.co.uk" + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" } ], - "description": "Restarts a process without Xdebug.", + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", "keywords": [ - "Xdebug", - "performance" + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" ], "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.4.0" }, "funding": [ { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", + "url": "https://github.com/amphp", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-05-06T16:37:16+00:00" + "time": "2025-01-19T15:43:40+00:00" }, { - "name": "evenement/evenement", - "version": "v3.0.2", + "name": "amphp/parallel", + "version": "v2.3.2", "source": { "type": "git", - "url": "https://github.com/igorw/evenement.git", - "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + "url": "https://github.com/amphp/parallel.git", + "reference": "321b45ae771d9c33a068186b24117e3cd1c48dce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", - "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "url": "https://api.github.com/repos/amphp/parallel/zipball/321b45ae771d9c33a068186b24117e3cd1c48dce", + "reference": "321b45ae771d9c33a068186b24117e3cd1c48dce", "shasum": "" }, "require": { - "php": ">=7.0" + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/process": "^2", + "amphp/serialization": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" }, "require-dev": { - "phpunit/phpunit": "^9 || ^6" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" }, "type": "library", "autoload": { + "files": [ + "src/Context/functions.php", + "src/Context/Internal/functions.php", + "src/Ipc/functions.php", + "src/Worker/functions.php" + ], "psr-4": { - "Evenement\\": "src/" + "Amp\\Parallel\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1572,53 +1557,65 @@ ], "authors": [ { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" } ], - "description": "Événement is a very simple event dispatching library for PHP", + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", "keywords": [ - "event-dispatcher", - "event-emitter" + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" ], "support": { - "issues": "https://github.com/igorw/evenement/issues", - "source": "https://github.com/igorw/evenement/tree/v3.0.2" + "issues": "https://github.com/amphp/parallel/issues", + "source": "https://github.com/amphp/parallel/tree/v2.3.2" }, - "time": "2023-08-08T05:53:35+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-08-27T21:55:40+00:00" }, { - "name": "fidry/cpu-core-counter", - "version": "1.2.0", + "name": "amphp/parser", + "version": "v1.1.1", "source": { "type": "git", - "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "8520451a140d3f46ac33042715115e290cf5785f" + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", - "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": ">=7.4" }, "require-dev": { - "fidry/makefile": "^0.2.0", - "fidry/php-cs-fixer-config": "^1.1.2", - "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^1.9.2", - "phpstan/phpstan-deprecation-rules": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.2.2", - "phpstan/phpstan-strict-rules": "^1.4.4", - "phpunit/phpunit": "^8.5.31 || ^9.5.26", - "webmozarts/strict-phpunit": "^7.5" + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" }, "type": "library", "autoload": { "psr-4": { - "Fidry\\CpuCoreCounter\\": "src/" + "Amp\\Parser\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1627,96 +1624,64 @@ ], "authors": [ { - "name": "Théo FIDRY", - "email": "theo.fidry@gmail.com" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "Tiny utility to get the number of CPU cores.", + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", "keywords": [ - "CPU", - "core" + "async", + "non-blocking", + "parser", + "stream" ], "support": { - "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" }, "funding": [ { - "url": "https://github.com/theofidry", + "url": "https://github.com/amphp", "type": "github" } ], - "time": "2024-08-06T10:04:20+00:00" + "time": "2024-03-21T19:16:53+00:00" }, { - "name": "friendsofphp/php-cs-fixer", - "version": "v3.68.0", + "name": "amphp/pipeline", + "version": "v1.2.3", "source": { "type": "git", - "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c" + "url": "https://github.com/amphp/pipeline.git", + "reference": "7b52598c2e9105ebcddf247fc523161581930367" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c", - "reference": "73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", + "reference": "7b52598c2e9105ebcddf247fc523161581930367", "shasum": "" }, "require": { - "clue/ndjson-react": "^1.0", - "composer/semver": "^3.4", - "composer/xdebug-handler": "^3.0.3", - "ext-filter": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "fidry/cpu-core-counter": "^1.2", - "php": "^7.4 || ^8.0", - "react/child-process": "^0.6.5", - "react/event-loop": "^1.0", - "react/promise": "^2.0 || ^3.0", - "react/socket": "^1.0", - "react/stream": "^1.0", - "sebastian/diff": "^4.0 || ^5.1 || ^6.0", - "symfony/console": "^5.4 || ^6.4 || ^7.0", - "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", - "symfony/finder": "^5.4 || ^6.4 || ^7.0", - "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", - "symfony/polyfill-mbstring": "^1.31", - "symfony/polyfill-php80": "^1.31", - "symfony/polyfill-php81": "^1.31", - "symfony/process": "^5.4 || ^6.4 || ^7.2", - "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" }, "require-dev": { - "facile-it/paraunit": "^1.3.1 || ^2.4", - "infection/infection": "^0.29.8", - "justinrainbow/json-schema": "^5.3 || ^6.0", - "keradus/cli-executor": "^2.1", - "mikey179/vfsstream": "^1.6.12", - "php-coveralls/php-coveralls": "^2.7", - "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", - "phpunit/phpunit": "^9.6.22 || ^10.5.40 || ^11.5.2", - "symfony/var-dumper": "^5.4.48 || ^6.4.15 || ^7.2.0", - "symfony/yaml": "^5.4.45 || ^6.4.13 || ^7.2.0" - }, - "suggest": { - "ext-dom": "For handling output formats in XML", - "ext-mbstring": "For handling non-UTF8 characters." + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" }, - "bin": [ - "php-cs-fixer" - ], - "type": "application", + "type": "library", "autoload": { "psr-4": { - "PhpCsFixer\\": "src/" - }, - "exclude-from-classmap": [ - "src/Fixer/Internal/*" - ] + "Amp\\Pipeline\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1724,288 +1689,2118 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { - "name": "Dariusz Rumiński", - "email": "dariusz.ruminski@gmail.com" + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "A tool to automatically fix PHP code style", + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", "keywords": [ - "Static code analysis", - "fixer", - "standards", - "static analysis" + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" ], "support": { - "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.68.0" + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.3" }, "funding": [ { - "url": "https://github.com/keradus", + "url": "https://github.com/amphp", "type": "github" } ], - "time": "2025-01-13T17:01:01+00:00" + "time": "2025-03-16T16:33:53+00:00" }, { - "name": "myclabs/deep-copy", - "version": "1.12.1", + "name": "amphp/process", + "version": "v2.0.3", "source": { "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "url": "https://github.com/amphp/process.git", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3 <3.2.2" + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpspec/prophecy": "^1.10", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" }, "type": "library", "autoload": { "files": [ - "src/DeepCopy/deep_copy.php" + "src/functions.php" ], "psr-4": { - "DeepCopy\\": "src/DeepCopy/" + "Amp\\Process\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.0.3" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" + "url": "https://github.com/amphp", + "type": "github" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2024-04-19T03:13:44+00:00" }, { - "name": "nikic/php-parser", - "version": "v5.4.0", + "name": "amphp/serialization", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", "shasum": "" }, "require": { - "ext-ctype": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "php": ">=7.4" + "php": ">=7.1" }, "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^9.0" + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" }, - "bin": [ - "bin/php-parse" - ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "PhpParser\\": "lib/PhpParser" + "Amp\\Serialization\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Nikita Popov" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "A PHP parser written in PHP", + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", "keywords": [ - "parser", - "php" + "async", + "asynchronous", + "serialization", + "serialize" ], "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2020-03-25T21:39:07+00:00" }, { - "name": "phar-io/manifest", - "version": "2.0.4", + "name": "amphp/socket", + "version": "v2.3.1", "source": { "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "54750ef60c58e43759730615a392c31c80e23176" + "url": "https://github.com/amphp/socket.git", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", - "reference": "54750ef60c58e43759730615a392c31c80e23176", + "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^6.5 | ^7", + "league/uri-interfaces": "^2.3 | ^7", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], + "psr-4": { + "Amp\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "support": { + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" } + ], + "time": "2024-04-21T14:33:03+00:00" + }, + { + "name": "amphp/sync", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" }, + "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" + "name": "Niklas Keller", + "email": "me@kelunik.com" }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" } ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-08-03T19:31:26+00:00" + }, + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "danog/advanced-json-rpc", + "version": "v3.2.2", + "source": { + "type": "git", + "url": "https://github.com/danog/php-advanced-json-rpc.git", + "reference": "aadb1c4068a88c3d0530cfe324b067920661efcb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/danog/php-advanced-json-rpc/zipball/aadb1c4068a88c3d0530cfe324b067920661efcb", + "reference": "aadb1c4068a88c3d0530cfe324b067920661efcb", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^5", + "php": ">=8.1", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + }, + "replace": { + "felixfbecker/php-advanced-json-rpc": "^3" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + }, + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" + } + ], + "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/danog/php-advanced-json-rpc/issues", + "source": "https://github.com/danog/php-advanced-json-rpc/tree/v3.2.2" + }, + "time": "2025-02-14T10:55:15+00:00" + }, + { + "name": "daverandom/libdns", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "Required for IDN support" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LibDNS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "DNS protocol implementation written in pure PHP", + "keywords": [ + "dns" + ], + "support": { + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + }, + "time": "2024-04-12T12:12:48+00:00" + }, + { + "name": "dnoegel/php-xdg-base-dir", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "XdgBaseDir\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "implementation of xdg base directory specification for php", + "support": { + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + }, + "time": "2019-12-04T15:06:13+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "felixfbecker/language-server-protocol", + "version": "v1.5.3", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-language-server-protocol.git", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/phpstan": "*", + "squizlabs/php_codesniffer": "^3.1", + "vimeo/psalm": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "LanguageServerProtocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "PHP classes for the Language Server Protocol", + "keywords": [ + "language", + "microsoft", + "php", + "server" + ], + "support": { + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" + }, + "time": "2024-04-30T00:40:11+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.68.0", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c", + "reference": "73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.0", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.3", + "ext-filter": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.2", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.5", + "react/event-loop": "^1.0", + "react/promise": "^2.0 || ^3.0", + "react/socket": "^1.0", + "react/stream": "^1.0", + "sebastian/diff": "^4.0 || ^5.1 || ^6.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", + "symfony/finder": "^5.4 || ^6.4 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", + "symfony/polyfill-mbstring": "^1.31", + "symfony/polyfill-php80": "^1.31", + "symfony/polyfill-php81": "^1.31", + "symfony/process": "^5.4 || ^6.4 || ^7.2", + "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3.1 || ^2.4", + "infection/infection": "^0.29.8", + "justinrainbow/json-schema": "^5.3 || ^6.0", + "keradus/cli-executor": "^2.1", + "mikey179/vfsstream": "^1.6.12", + "php-coveralls/php-coveralls": "^2.7", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", + "phpunit/phpunit": "^9.6.22 || ^10.5.40 || ^11.5.2", + "symfony/var-dumper": "^5.4.48 || ^6.4.15 || ^7.2.0", + "symfony/yaml": "^5.4.45 || ^6.4.13 || ^7.2.0" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/Fixer/Internal/*" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.68.0" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2025-01-13T17:01:01+00:00" + }, + { + "name": "kelunik/certificate", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, + { + "name": "league/uri", + "version": "7.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "81fb5145d2644324614cc532b28efd0215bda430" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.5", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:40:02+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:18:47+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "netresearch/jsonmapper", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", + "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v5.0.0" + }, + "time": "2024-09-08T10:20:00+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + }, + "time": "2025-10-21T19:32:17+00:00" + }, + { + "name": "phar-io/composer-distributor", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/phar-io/composer-distributor.git", + "reference": "dd7d936290b2a42b0c64bfe08090b5c597c280c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/composer-distributor/zipball/dd7d936290b2a42b0c64bfe08090b5c597c280c9", + "reference": "dd7d936290b2a42b0c64bfe08090b5c597c280c9", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1 || ^2.0", + "ext-dom": "*", + "ext-libxml": "*", + "phar-io/filesystem": "^2.0", + "phar-io/gnupg": "^1.0", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "phpunit/phpunit": "^9.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "PharIo\\ComposerDistributor\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Heigl", + "email": "andreas@heigl.org", + "role": "Developer" + }, + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Feldmann", + "email": "sf@sebastian-feldmann.info", + "role": "Developer" + } + ], + "description": "Base Code for a composer plugin that installs PHAR-files", + "homepage": "https://phar.io", + "keywords": [ + "bin", + "binary", + "composer", + "distribute", + "phar", + "phive" + ], + "support": { + "issues": "https://github.com/phar-io/composer-distributor/issues", + "source": "https://github.com/phar-io/composer-distributor/tree/1.0.2" + }, + "funding": [ + { + "url": "https://phar.io", + "type": "other" + } + ], + "time": "2023-05-31T17:05:49+00:00" + }, + { + "name": "phar-io/executor", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/executor.git", + "reference": "5bfb7400224a0c1cf83343660af85c7f5a073473" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/executor/zipball/5bfb7400224a0c1cf83343660af85c7f5a073473", + "reference": "5bfb7400224a0c1cf83343660af85c7f5a073473", + "shasum": "" + }, + "require": { + "phar-io/filesystem": "^2.0", + "php": "^7.2||^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + } + ], + "support": { + "issues": "https://github.com/phar-io/executor/issues", + "source": "https://github.com/phar-io/executor/tree/1.0.1" + }, + "time": "2020-11-30T10:53:57+00:00" + }, + { + "name": "phar-io/filesystem", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/filesystem.git", + "reference": "222e3ea432262a05706b7066697c21257664d9d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/filesystem/zipball/222e3ea432262a05706b7066697c21257664d9d1", + "reference": "222e3ea432262a05706b7066697c21257664d9d1", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + } + ], + "support": { + "issues": "https://github.com/phar-io/filesystem/issues", + "source": "https://github.com/phar-io/filesystem/tree/2.0.1" + }, + "time": "2020-11-30T10:16:22+00:00" + }, + { + "name": "phar-io/gnupg", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/gnupg.git", + "reference": "ed8ab1740ac4e9db99500e7252911f2821357093" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/gnupg/zipball/ed8ab1740ac4e9db99500e7252911f2821357093", + "reference": "ed8ab1740ac4e9db99500e7252911f2821357093", + "shasum": "" + }, + "require": { + "phar-io/executor": "^1.0", + "phar-io/filesystem": "^2.0", + "php": "^7.2||^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + } + ], + "description": "Thin GnuPG wrapper class around the gnupg binary, mimicking the pecl/gnupg api", + "support": { + "issues": "https://github.com/phar-io/gnupg/issues", + "source": "https://github.com/phar-io/gnupg/tree/1.0.3" + }, + "time": "2024-08-22T20:45:57+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.1" + }, + "time": "2024-12-07T09:39:29+00:00" + }, + { + "name": "phpdocumentor/shim", + "version": "v3.8.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/shim.git", + "reference": "9a74290f4acca84361589ecd052878243f064333" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/shim/zipball/9a74290f4acca84361589ecd052878243f064333", + "reference": "9a74290f4acca84361589ecd052878243f064333", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "phar-io/composer-distributor": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "phpDocumentor\\Plugin" + }, + "autoload": { + "psr-4": { + "phpDocumentor\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "source": "https://github.com/phpDocumentor/shim/tree/v3.8.1" + }, + "time": "2025-07-21T19:34:24+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + }, + "time": "2024-11-09T15:12:26+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.4" + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2024-03-03T12:33:53+00:00" + "time": "2025-08-30T15:50:23+00:00" }, { - "name": "phar-io/version", - "version": "3.2.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" - }, + "name": "phpstan/phpstan", + "version": "2.1.32", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227", + "reference": "e126cad1e30a99b137b8ed75a85a676450ebb227", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" }, + "bin": [ + "phpstan", + "phpstan.phar" + ], "type": "library", "autoload": { - "classmap": [ - "src/" + "files": [ + "bootstrap.php" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" + "url": "https://github.com/ondrejmirtes", + "type": "github" }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" + "url": "https://github.com/phpstan", + "type": "github" } ], - "description": "Library for handling version information and constraints", - "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" - }, - "time": "2022-02-21T01:04:05+00:00" + "time": "2025-11-11T15:18:17+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.8", + "version": "11.0.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "418c59fd080954f8c4aa5631d9502ecda2387118" + "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118", - "reference": "418c59fd080954f8c4aa5631d9502ecda2387118", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.3.1", + "nikic/php-parser": "^5.4.0", "php": ">=8.2", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-text-template": "^4.0.1", @@ -2017,7 +3812,7 @@ "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^11.5.0" + "phpunit/phpunit": "^11.5.2" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -2055,15 +3850,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.8" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" } ], - "time": "2024-12-11T12:34:27+00:00" + "time": "2025-08-27T14:37:49+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2861,23 +4668,23 @@ }, { "name": "react/promise", - "version": "v3.2.0", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", "shasum": "" }, "require": { "php": ">=7.1.0" }, "require-dev": { - "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpstan/phpstan": "1.12.28 || 1.4.10", "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", @@ -2922,7 +4729,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.2.0" + "source": "https://github.com/reactphp/promise/tree/v3.3.0" }, "funding": [ { @@ -2930,7 +4737,7 @@ "type": "open_collective" } ], - "time": "2024-05-24T10:39:05+00:00" + "time": "2025-08-19T18:57:03+00:00" }, { "name": "react/socket", @@ -3090,6 +4897,78 @@ ], "time": "2024-06-11T12:45:25+00:00" }, + { + "name": "revolt/event-loop", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" + }, + "time": "2025-01-25T19:27:39+00:00" + }, { "name": "sebastian/cli-parser", "version": "3.0.2", @@ -3149,16 +5028,16 @@ }, { "name": "sebastian/code-unit", - "version": "3.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca" + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", "shasum": "" }, "require": { @@ -3194,7 +5073,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" }, "funding": [ { @@ -3202,7 +5081,7 @@ "type": "github" } ], - "time": "2024-12-12T09:59:06+00:00" + "time": "2025-03-19T07:56:08+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -3262,16 +5141,16 @@ }, { "name": "sebastian/comparator", - "version": "6.3.0", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115" + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/d4e47a769525c4dd38cea90e5dcd435ddbbc7115", - "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", "shasum": "" }, "require": { @@ -3290,7 +5169,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.2-dev" + "dev-main": "6.3-dev" } }, "autoload": { @@ -3330,15 +5209,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.0" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2025-01-06T10:28:19+00:00" + "time": "2025-08-10T08:07:46+00:00" }, { "name": "sebastian/complexity", @@ -3467,23 +5358,23 @@ }, { "name": "sebastian/environment", - "version": "7.2.0", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "suggest": { "ext-posix": "*" @@ -3519,28 +5410,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2024-07-03T04:54:44+00:00" + "time": "2025-05-21T11:55:47+00:00" }, { "name": "sebastian/exporter", - "version": "6.3.0", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", "shasum": "" }, "require": { @@ -3554,7 +5457,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.1-dev" + "dev-main": "6.3-dev" } }, "autoload": { @@ -3597,15 +5500,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-12-05T09:17:50+00:00" + "time": "2025-09-24T06:12:51+00:00" }, { "name": "sebastian/global-state", @@ -3843,23 +5758,23 @@ }, { "name": "sebastian/recursion-context", - "version": "6.0.2", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", - "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { @@ -3895,28 +5810,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2024-07-03T05:10:34+00:00" + "time": "2025-08-13T04:42:22+00:00" }, { "name": "sebastian/type", - "version": "5.1.0", + "version": "5.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac" + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", "shasum": "" }, "require": { @@ -3938,83 +5865,235 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "spatie/array-to-xml", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/array-to-xml.git", + "reference": "6a740f39415aee8886aea10333403adc77d50791" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/6a740f39415aee8886aea10333403adc77d50791", + "reference": "6a740f39415aee8886aea10333403adc77d50791", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "pestphp/pest": "^1.21", + "spatie/pest-plugin-snapshots": "^1.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spatie\\ArrayToXml\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://freek.dev", + "role": "Developer" } ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", + "description": "Convert an array to xml", + "homepage": "https://github.com/spatie/array-to-xml", + "keywords": [ + "array", + "convert", + "xml" + ], "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.0" + "source": "https://github.com/spatie/array-to-xml/tree/3.4.1" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", "type": "github" } ], - "time": "2024-09-17T13:12:04+00:00" + "time": "2025-11-12T10:32:50+00:00" }, { - "name": "sebastian/version", - "version": "5.0.2", + "name": "spatie/typescript-transformer", + "version": "2.5.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + "url": "https://github.com/spatie/typescript-transformer.git", + "reference": "dd7cbb90b6b8c34f2aee68701cf39c5432400c0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "url": "https://api.github.com/repos/spatie/typescript-transformer/zipball/dd7cbb90b6b8c34f2aee68701cf39c5432400c0d", + "reference": "dd7cbb90b6b8c34f2aee68701cf39c5432400c0d", "shasum": "" }, "require": { - "php": ">=8.2" + "nikic/php-parser": "^4.18|^5.0", + "php": "^8.1", + "phpdocumentor/type-resolver": "^1.6.2", + "symfony/process": "^5.2|^6.0|^7.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.0-dev" - } + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.40", + "larapack/dd": "^1.1", + "myclabs/php-enum": "^1.7", + "pestphp/pest": "^1.22", + "phpstan/extension-installer": "^1.1", + "phpunit/phpunit": "^9.0", + "spatie/data-transfer-object": "^2.0", + "spatie/enum": "^3.0", + "spatie/pest-plugin-snapshots": "^1.1", + "spatie/temporary-directory": "^1.2|^2.0" }, + "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Spatie\\TypeScriptTransformer\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" } ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", + "description": "Transform your PHP structures to TypeScript types", + "homepage": "https://github.com/spatie/typescript-transformer", + "keywords": [ + "spatie", + "typescript-transformer" + ], "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + "issues": "https://github.com/spatie/typescript-transformer/issues", + "source": "https://github.com/spatie/typescript-transformer/tree/2.5.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", "type": "github" } ], - "time": "2024-10-09T05:16:32+00:00" + "time": "2025-04-25T13:53:57+00:00" }, { "name": "staabm/side-effects-detector", @@ -4070,23 +6149,24 @@ }, { "name": "symfony/console", - "version": "v7.2.1", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0" + "symfony/string": "^7.2" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -4143,7 +6223,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.1" + "source": "https://github.com/symfony/console/tree/v7.3.6" }, "funding": [ { @@ -4154,25 +6234,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-11T03:49:26+00:00" + "time": "2025-11-04T01:21:42+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.2.0", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", "shasum": "" }, "require": { @@ -4223,7 +6307,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" }, "funding": [ { @@ -4234,25 +6318,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-08-13T11:49:31+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { @@ -4266,7 +6354,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4299,7 +6387,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -4315,20 +6403,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/filesystem", - "version": "v7.2.0", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a", + "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a", "shasum": "" }, "require": { @@ -4365,7 +6453,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.2.0" + "source": "https://github.com/symfony/filesystem/tree/v7.3.6" }, "funding": [ { @@ -4376,25 +6464,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:15:23+00:00" + "time": "2025-11-05T09:52:27+00:00" }, { "name": "symfony/finder", - "version": "v7.2.2", + "version": "v7.3.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb" + "reference": "9f696d2f1e340484b4683f7853b273abff94421f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb", + "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", + "reference": "9f696d2f1e340484b4683f7853b273abff94421f", "shasum": "" }, "require": { @@ -4429,7 +6521,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.2.2" + "source": "https://github.com/symfony/finder/tree/v7.3.5" }, "funding": [ { @@ -4440,25 +6532,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-30T19:00:17+00:00" + "time": "2025-10-15T18:45:57+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.2.0", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50" + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50", - "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", "shasum": "" }, "require": { @@ -4496,7 +6592,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.2.0" + "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" }, "funding": [ { @@ -4507,16 +6603,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-20T11:17:29+00:00" + "time": "2025-08-05T10:16:07+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -4575,7 +6675,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -4586,6 +6686,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -4595,16 +6699,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -4653,7 +6757,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -4664,16 +6768,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -4734,7 +6842,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -4745,6 +6853,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -4754,16 +6866,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -4814,7 +6926,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -4825,16 +6937,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -4890,7 +7006,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" }, "funding": [ { @@ -4901,6 +7017,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -4908,18 +7028,98 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, { "name": "symfony/process", - "version": "v7.2.0", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", "shasum": "" }, "require": { @@ -4951,7 +7151,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.0" + "source": "https://github.com/symfony/process/tree/v7.3.4" }, "funding": [ { @@ -4962,25 +7162,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -4998,7 +7202,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -5034,7 +7238,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -5045,25 +7249,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/stopwatch", - "version": "v7.2.2", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "e46690d5b9d7164a6d061cab1e8d46141b9f49df" + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/e46690d5b9d7164a6d061cab1e8d46141b9f49df", - "reference": "e46690d5b9d7164a6d061cab1e8d46141b9f49df", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", "shasum": "" }, "require": { @@ -5096,7 +7304,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.2.2" + "source": "https://github.com/symfony/stopwatch/tree/v7.3.0" }, "funding": [ { @@ -5112,20 +7320,20 @@ "type": "tidelift" } ], - "time": "2024-12-18T14:28:33+00:00" + "time": "2025-02-24T10:49:57+00:00" }, { "name": "symfony/string", - "version": "v7.2.0", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + "reference": "f96476035142921000338bad71e5247fbc138872" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", "shasum": "" }, "require": { @@ -5140,7 +7348,6 @@ }, "require-dev": { "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -5183,7 +7390,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.0" + "source": "https://github.com/symfony/string/tree/v7.3.4" }, "funding": [ { @@ -5194,25 +7401,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-13T13:31:26+00:00" + "time": "2025-09-11T14:36:48+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -5241,7 +7452,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -5249,7 +7460,183 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" + }, + { + "name": "vimeo/psalm", + "version": "6.13.1", + "source": { + "type": "git", + "url": "https://github.com/vimeo/psalm.git", + "reference": "1e3b7f0a8ab32b23197b91107adc0a7ed8a05b51" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/1e3b7f0a8ab32b23197b91107adc0a7ed8a05b51", + "reference": "1e3b7f0a8ab32b23197b91107adc0a7ed8a05b51", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/parallel": "^2.3", + "composer-runtime-api": "^2", + "composer/semver": "^1.4 || ^2.0 || ^3.0", + "composer/xdebug-handler": "^2.0 || ^3.0", + "danog/advanced-json-rpc": "^3.1", + "dnoegel/php-xdg-base-dir": "^0.1.1", + "ext-ctype": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-tokenizer": "*", + "felixfbecker/language-server-protocol": "^1.5.3", + "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", + "netresearch/jsonmapper": "^5.0", + "nikic/php-parser": "^5.0.0", + "php": "~8.1.31 || ~8.2.27 || ~8.3.16 || ~8.4.3", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0 || ^7.0", + "spatie/array-to-xml": "^2.17.0 || ^3.0", + "symfony/console": "^6.0 || ^7.0", + "symfony/filesystem": "~6.3.12 || ~6.4.3 || ^7.0.3", + "symfony/polyfill-php84": "^1.31.0" + }, + "provide": { + "psalm/psalm": "self.version" + }, + "require-dev": { + "amphp/phpunit-util": "^3", + "bamarni/composer-bin-plugin": "^1.4", + "brianium/paratest": "^6.9", + "danog/class-finder": "^0.4.8", + "dg/bypass-finals": "^1.5", + "ext-curl": "*", + "mockery/mockery": "^1.5", + "nunomaduro/mock-final-classes": "^1.1", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpdoc-parser": "^1.6", + "phpunit/phpunit": "^9.6", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.19", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.6", + "symfony/process": "^6.0 || ^7.0" + }, + "suggest": { + "ext-curl": "In order to send data to shepherd", + "ext-igbinary": "^2.0.5 is required, used to serialize caching data" + }, + "bin": [ + "psalm", + "psalm-language-server", + "psalm-plugin", + "psalm-refactor", + "psalm-review", + "psalter" + ], + "type": "project", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev", + "dev-3.x": "3.x-dev", + "dev-4.x": "4.x-dev", + "dev-5.x": "5.x-dev", + "dev-6.x": "6.x-dev", + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psalm\\": "src/Psalm/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matthew Brown" + }, + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" + } + ], + "description": "A static analysis tool for finding errors in PHP applications", + "keywords": [ + "code", + "inspection", + "php", + "static analysis" + ], + "support": { + "docs": "https://psalm.dev/docs", + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm" + }, + "time": "2025-08-06T10:10:28+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^7.2 || ^8.0" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.12.1" + }, + "time": "2025-10-29T15:56:20+00:00" } ], "aliases": [], @@ -5259,5 +7646,5 @@ "prefer-lowest": false, "platform": {}, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/conf/config.docker.ini.php b/packages/backend-php/conf/config.docker.ini.php similarity index 84% rename from conf/config.docker.ini.php rename to packages/backend-php/conf/config.docker.ini.php index 930b6d825..a2c279059 100644 --- a/conf/config.docker.ini.php +++ b/packages/backend-php/conf/config.docker.ini.php @@ -33,12 +33,6 @@ define('OAUTH_GOOGLE_CLIENT', NOMOS_OAUTH_GOOGLE_CLIENT); define('OAUTH_GOOGLE_SECRET', NOMOS_OAUTH_GOOGLE_SECRET); -define('RABBITMQ_HOST', NOMOS_RABBITMQ_HOST); -define('RABBITMQ_PORT', NOMOS_RABBITMQ_PORT); -define('RABBITMQ_USER', NOMOS_RABBITMQ_USER); -define('RABBITMQ_PASSWORD', NOMOS_RABBITMQ_PASSWORD); -define('RABBITMQ_VHOST', NOMOS_RABBITMQ_VHOST); - define('STRIPE_API_KEY', NOMOS_STRIPE_API_KEY); define('STRIPE_WEBHOOK_SECRET', NOMOS_STRIPE_WEBHOOK_SECRET); define('STRIPE_PRODUCTS', json_decode(NOMOS_STRIPE_PRODUCTS, true)); @@ -50,4 +44,4 @@ * Show MySql Errors. * Not recomended for live site. true/false. */ -define('DEBUG', false); +define('DEBUG', defined('NOMOS_DEBUG')); diff --git a/packages/backend-php/conf/config.ini.template.php b/packages/backend-php/conf/config.ini.template.php new file mode 100644 index 000000000..c1c0e0de2 --- /dev/null +++ b/packages/backend-php/conf/config.ini.template.php @@ -0,0 +1,48 @@ + [ + 'item_name' => 'VHS Keyholder Membership', + 'item_number' => 'vhs_membership_keyholder' + ] +]); + +/** + * Show MySql Errors. + * Not recomended for live site. true/false. + */ +define('DEBUG', defined('NOMOS_DEBUG')); diff --git a/conf/nginx-vhost-docker.conf b/packages/backend-php/conf/nginx.conf similarity index 68% rename from conf/nginx-vhost-docker.conf rename to packages/backend-php/conf/nginx.conf index 70ef1db45..eabd1709a 100644 --- a/conf/nginx-vhost-docker.conf +++ b/packages/backend-php/conf/nginx.conf @@ -1,18 +1,16 @@ server { - set $app_root "/www"; + set $app_root "/var/www/sites/membership.vanhack.ca"; - listen 80 default_server; - server_name _; + listen 80; + server_name membership.vanhack.ca; - location ~ (/services/|\.php$) { + location ~ (/services/) { include fastcgi_params; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_read_timeout 300; - fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; + fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $app_root/app/app.php; # ?service=$fastcgi_script_name; - fastcgi_buffers 16 16k; - fastcgi_buffer_size 32k; } @@ -29,4 +27,7 @@ server { include fastcgi_params; fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; } + + include mime.types; } + diff --git a/packages/backend-php/conf/php-fpm/00-defaults.conf b/packages/backend-php/conf/php-fpm/00-defaults.conf new file mode 100644 index 000000000..5d1b88c1b --- /dev/null +++ b/packages/backend-php/conf/php-fpm/00-defaults.conf @@ -0,0 +1,11 @@ +[global] +error_log=/proc/self/fd/2 +log_limit=8192 + +[www] +access.log=/proc/self/fd/2 +clear_env=no +catch_workers_output=yes +decorate_workers_output=no + +listen=0.0.0.0:9000 diff --git a/packages/backend-php/conf/php-fpm/99-no-daemonize.conf b/packages/backend-php/conf/php-fpm/99-no-daemonize.conf new file mode 100644 index 000000000..a53c0e5a3 --- /dev/null +++ b/packages/backend-php/conf/php-fpm/99-no-daemonize.conf @@ -0,0 +1,2 @@ +[global] +daemonize=no diff --git a/packages/backend-php/conf/php/99-no-fastcgi-logging.ini b/packages/backend-php/conf/php/99-no-fastcgi-logging.ini new file mode 100644 index 000000000..7e9000b48 --- /dev/null +++ b/packages/backend-php/conf/php/99-no-fastcgi-logging.ini @@ -0,0 +1 @@ +fastcgi.logging=Off diff --git a/packages/backend-php/conf/php/99-opcache.ini b/packages/backend-php/conf/php/99-opcache.ini new file mode 100644 index 000000000..931496787 --- /dev/null +++ b/packages/backend-php/conf/php/99-opcache.ini @@ -0,0 +1,2 @@ +[opcache] +opcache.enable=1 diff --git a/packages/backend-php/conf/php/99-sessions-dir.ini b/packages/backend-php/conf/php/99-sessions-dir.ini new file mode 100644 index 000000000..0c9a6a532 --- /dev/null +++ b/packages/backend-php/conf/php/99-sessions-dir.ini @@ -0,0 +1 @@ +session.save_path="/sessions" diff --git a/packages/backend-php/docker/docker_compose_run.sh b/packages/backend-php/docker/docker_compose_run.sh new file mode 100755 index 000000000..1c3b3189b --- /dev/null +++ b/packages/backend-php/docker/docker_compose_run.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# shellcheck shell=bash + +if [ ! -d /sessions ]; then + mkdir -p /sessions +fi + +chown nobody:nobody /sessions && chmod 1777 /sessions + +/usr/local/bin/docker_env_config.sh + +(cd /var/www/html/tools && php migrate.php -b -m -t) + +CMD="$*" + +if [ "${CMD:0:1}" = "-" ]; then + CMD="php-fpm83 ${CMD}" +fi + +exec "${CMD}" diff --git a/packages/backend-php/docker/docker_env_config.sh b/packages/backend-php/docker/docker_env_config.sh new file mode 100755 index 000000000..3b5ee133a --- /dev/null +++ b/packages/backend-php/docker/docker_env_config.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +{ + echo " /var/www/html/conf/env.php diff --git a/packages/backend-php/docker/docker_env_config_local.sh b/packages/backend-php/docker/docker_env_config_local.sh new file mode 100755 index 000000000..13b071826 --- /dev/null +++ b/packages/backend-php/docker/docker_env_config_local.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +echo " /www/conf/env.php -service php5-fpm start +service php83-fpm start nginx diff --git a/packages/backend-php/justfile b/packages/backend-php/justfile new file mode 100644 index 000000000..2d2628c34 --- /dev/null +++ b/packages/backend-php/justfile @@ -0,0 +1,73 @@ +set export := true + +help: + @just -l + +format target: + @echo 'Formatting {{target}}…' + @just "format_{{target}}" + +format_all: + #!/usr/bin/env bash + set -eo pipefail + + echo ${FILES:-.} | xargs -n1 | grep -v -E $(find . -type l | grep -vw node_modules | cut -f2- -d/ | xargs | tr ' ' '|') | xargs pnpm exec prettier -w + +format_php: + #!/usr/bin/env bash + set -eo pipefail + + echo "${FILES:-app/ tests/ tools/ vhs/}" | sed 's:packages/backend-php/::g' | xargs vendor/bin/php-cs-fixer fix --config=./.php-cs-fixer.php + +install target: + @echo 'Installing {{target}}…' + @just "install_{{target}}" + +install_composer: + #!/usr/bin/env bash + set -euo pipefail + + if [ ! -f ./tools/composer.phar ]; then + TMPFILE=$(mktemp) + + EXPECTED_CHECKSUM="$(php -r 'copy("https://composer.github.io/installer.sig", "php://stdout");')" + php -r "copy('https://getcomposer.org/installer', '${TMPFILE}');" + ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', '${TMPFILE}');")" + + if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then + echo >&2 'ERROR: Invalid installer checksum' + rm "${TMPFILE}" + exit 1 + fi + + php "${TMPFILE}" --install-dir ./tools --quiet + rm "${TMPFILE}" + else + echo "composer has already been set up!" + fi + + ./tools/composer.sh install ${COMPOSER_INSTALL_OPT:-} + +run_composer: + echo "Running composer" + ./tools/composer.sh install + +setup target: + @echo 'Setting up {{target}}…' + @just "setup_{{target}}" + +setup_vendor: install_composer run_composer + +test target: + @echo 'Testing {{target}}…' + @just "test_{{target}}" + +test_php: + #!/usr/bin/env bash + set -eo pipefail + + vendor/bin/phpunit ${FILES:-app/ tests/ tools/ vhs/} + +update target: + @echo 'Updating {{target}}…' + @just "update_{{target}}" diff --git a/packages/backend-php/package.json b/packages/backend-php/package.json new file mode 100644 index 000000000..9a64ac6ec --- /dev/null +++ b/packages/backend-php/package.json @@ -0,0 +1,44 @@ +{ + "author": "", + "dependencies": { + "just-install": "^2.0.2" + }, + "description": "NOMOS Our Membership Operations Software. In greek mythology, Nomos is the personified spirit of law.", + "devDependencies": { + "@prettier/plugin-php": "^0.22.2", + "@prettier/plugin-xml": "^3.4.1", + "@tyisi/config-eslint": "^4.0.0", + "@tyisi/config-prettier": "^1.0.1", + "@types/node": "^22.13.1", + "bower": "^1.8.14", + "eslint": "^9.15.0", + "prettier": "^3.0.3", + "prettier-plugin-ini": "^1.3.0", + "prettier-plugin-nginx": "^1.0.3", + "prettier-plugin-sh": "^0.14.0", + "prettier-plugin-sql": "^0.18.1", + "prettier-plugin-tailwindcss": "^0.6.11" + }, + "keywords": [], + "license": "ISC", + "main": "index.js", + "name": "@vhs/nomos-backend-php", + "private": true, + "scripts": { + "format:php": "just format php", + "prepare": "npm run prepare:vendor", + "prepare:vendor": "just setup vendor", + "test": "npm run test:php", + "test:php": "just test php" + }, + "version": "1.0.0", + "files": [ + "app/**/*.php", + "conf/**/*", + "docker/*.sh", + "vhs/**/*.php", + "justfile", + "tools/*", + "composer.*" + ] +} diff --git a/packages/backend-php/php-ts-transformer.php b/packages/backend-php/php-ts-transformer.php new file mode 100644 index 000000000..f7633a658 --- /dev/null +++ b/packages/backend-php/php-ts-transformer.php @@ -0,0 +1,34 @@ +autoDiscoverTypes(__DIR__ . '/app') + // ->autoDiscoverTypes(__DIR__ . '/vhs/Cloneable.php') + // ->autoDiscoverTypes(__DIR__ . '/vhs/Logger.php') + // ->autoDiscoverTypes(__DIR__ . '/vhs/Singleton.php') + // ->autoDiscoverTypes(__DIR__ . '/vhs/SplClassLoader.php') + ->autoDiscoverTypes(__DIR__ . '/vhs/database') + ->autoDiscoverTypes(__DIR__ . '/vhs/domain') + ->autoDiscoverTypes(__DIR__ . '/vhs/loggers') + // ->autoDiscoverTypes(__DIR__ . '/vhs/messaging') + ->autoDiscoverTypes(__DIR__ . '/vhs/migration') + ->autoDiscoverTypes(__DIR__ . '/vhs/monitors') + ->autoDiscoverTypes(__DIR__ . '/vhs/security') + ->autoDiscoverTypes(__DIR__ . '/vhs/services') + // ->autoDiscoverTypes(__DIR__ . '/vhs/vhs.php') + ->autoDiscoverTypes(__DIR__ . '/vhs/web') + // list of transformers + ->transformers([InterfaceTransformer::class, EnumTransformer::class, DtoTransformer::class]) + // file where TypeScript type definitions will be written + ->outputFile(__DIR__ . '/packages/frontend-react/src/types/nomos.d.ts'); + +TypeScriptTransformer::create($config)->transform(); diff --git a/packages/backend-php/phpstan.neon b/packages/backend-php/phpstan.neon new file mode 100644 index 000000000..3420b19db --- /dev/null +++ b/packages/backend-php/phpstan.neon @@ -0,0 +1,23 @@ +parameters: + level: 6 + paths: + - app/ + - tests/ + - tools/ + - vhs/ + + bootstrapFiles: + - conf/config.ini.php + - app/include.php + + scanFiles: + - conf/config.ini.php + - app/include.php + - vhs/vhs.php + + treatPhpDocTypesAsCertain: false + + excludePaths: + - app/utils/converters/PHP2TS.php + - '**/.vscode-server/' + - vendor/ diff --git a/phpunit.xml b/packages/backend-php/phpunit.xml similarity index 100% rename from phpunit.xml rename to packages/backend-php/phpunit.xml diff --git a/packages/backend-php/psalm.xml b/packages/backend-php/psalm.xml new file mode 100644 index 000000000..7c856751f --- /dev/null +++ b/packages/backend-php/psalm.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/packages/backend-php/tests/ConstantsTest.php b/packages/backend-php/tests/ConstantsTest.php new file mode 100644 index 000000000..403f9f4a7 --- /dev/null +++ b/packages/backend-php/tests/ConstantsTest.php @@ -0,0 +1,92 @@ +assertEquals('Y-m-d 00:00:00', Formats::DATE_TIME_ISO_SHORT_MIDNIGHT); + } + + /** + * test_DateTime_DATE_TIME_SIMPLE. + * + * @return void + */ + public function test_DateTime_DATE_TIME_SIMPLE(): void { + $this->assertEquals('Y-m-d H:i:s', Formats::DATE_TIME_ISO_SHORT_FULL); + } + + /** + * test_Errors_E_INVALID_PASSWORD_HASH. + * + * @return void + */ + public function test_Errors_E_INVALID_PASSWORD_HASH(): void { + $this->assertEquals('Invalid password hash', Errors::E_INVALID_PASSWORD_HASH); + } + + /** + * test_StringLiterals_AuthAccessDenied. + * + * @return void + */ + public function test_StringLiterals_AuthAccessDenied(): void { + $this->assertEquals('Access Denied', StringLiterals::AUTH_ACCESS_DENIED); + } + + /** + * test_StringLiterals_AuthAccessGranted. + * + * @return void + */ + public function test_StringLiterals_AuthAccessGranted(): void { + $this->assertEquals('Access Granted', StringLiterals::AUTH_ACCESS_GRANTED); + } + + /** + * test_StringLiterals_HTTP_PREFIX. + * + * @return void + */ + public function test_StringLiterals_HTTP_PREFIX(): void { + $this->assertEquals('http://', StringLiterals::HTTP_PREFIX); + } + + /** + * test_StringLiterals_HTTPS_PREFIX. + * + * @return void + */ + public function test_StringLiterals_HTTPS_PREFIX(): void { + $this->assertEquals('https://', StringLiterals::HTTPS_PREFIX); + } + + protected function setUp(): void { + // Override + } + + /** + * tearDown. + * + * @return void + */ + protected function tearDown(): void { + // Override + } +} diff --git a/tests/DomainTest.php b/packages/backend-php/tests/DomainTest.php similarity index 76% rename from tests/DomainTest.php rename to packages/backend-php/tests/DomainTest.php index b2dd3da29..fec78eb6c 100644 --- a/tests/DomainTest.php +++ b/packages/backend-php/tests/DomainTest.php @@ -8,68 +8,36 @@ */ use PHPUnit\Framework\TestCase; -use vhs\database\constraints\Constraint; -use vhs\database\Table; -use vhs\database\types\Type; +use tests\domain\ExampleDomain; +use tests\schema\ExampleSchema; use vhs\database\wheres\Where; -use vhs\domain\Domain; -use vhs\domain\Schema; -use vhs\domain\validations\ValidationFailure; -use vhs\domain\validations\ValidationResults; -class ExampleSchema extends Schema { +/** @typescript */ +class DomainTest extends TestCase { /** - * @return Table + * inMemoryEngine. + * + * @var mixed */ - public static function init() { - $table = new Table('example', null); - - $table->addColumn('id', Type::Int()); - $table->addColumn('testA', Type::String(true)); - $table->addColumn('testB', Type::String(true)); - $table->addColumn('testC', Type::String(true)); - - $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); - - return $table; - } -} - -class ExampleDomain extends Domain { - public static function Define() { - ExampleDomain::Schema(ExampleSchema::Type()); - } - - public function get_magic() { - return 'magic field'; - } - - public function get_testC() { - return $this->internal_testC . 'fail'; - } - - public function set_magic($value) { - $this->testC = $value . 'magic'; - } - - public function set_testC($value) { - $this->internal_testC = $value . 'pass'; - } - - public function validate(ValidationResults &$results) { - if ($this->testA != 'pass') { - $results->add(new ValidationFailure('testA is not equal to pass')); - } - } -} - -class DomainTest extends TestCase { private static $inMemoryEngine; + + /** + * $logger. + * + * @var \vhs\Logger + */ private static $logger; - private static $mySqlEngine; - public function stuff() { + // private static $mySqlEngine; + + /** + * stuff. + * + * @return void + */ + public function stuff(): void { $eg = new ExampleDomain(); + $eg->testA = 'pass'; $eg->testB = 'blimey'; @@ -79,6 +47,7 @@ public function stuff() { unset($eg); + /** @var ExampleDomain */ $eg = ExampleDomain::find(['id' => 1]); $this->assertEquals('blimey', $eg->testB); @@ -106,6 +75,7 @@ public function stuff() { $eg3->testB = 'eg'; $eg3->save(); + /** @var ExampleDomain[] */ $records = ExampleDomain::where(Where::Equal(ExampleSchema::Columns()->testB, 'eg1')); $this->assertEquals(1, sizeof($records)); @@ -127,7 +97,12 @@ public function stuff() { $this->assertEquals('eg', $records[1]->testB); } - public function test_childRelationship() { + /** + * test_childRelationship. + * + * @return void + */ + public function test_childRelationship(): void { \vhs\database\Database::setEngine(self::$inMemoryEngine); $knight = new \tests\domain\Knight(); @@ -145,6 +120,7 @@ public function test_childRelationship() { unset($knight); + /** @var \tests\domain\Knight */ $knight = \tests\domain\Knight::find($knightid); $this->assertEquals(1, count($knight->rings->all())); @@ -158,12 +134,22 @@ public function test_childRelationship() { //\vhs\database\Database::arbitrary("DROP TABLE example;"); //} - public function test_InMemoryDomainTest() { + /** + * test_InMemoryDomainTest. + * + * @return void + */ + public function test_InMemoryDomainTest(): void { \vhs\database\Database::setEngine(self::$inMemoryEngine); $this->stuff(); } - public function test_parentRelationship() { + /** + * test_parentRelationship. + * + * @return void + */ + public function test_parentRelationship(): void { \vhs\database\Database::setEngine(self::$inMemoryEngine); $sword = new \tests\domain\Sword(); @@ -182,7 +168,11 @@ public function test_parentRelationship() { unset($knight); - $knight = \tests\domain\Knight::where(Where::Equal(\tests\domain\Knight::Schema()->Columns()->name, 'Black Knight')); + $knight = \tests\domain\Knight::where( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(\tests\domain\Knight::Schema()->Columns()->name, 'Black Knight') + ); $this->assertEquals(1, count($knight)); @@ -194,7 +184,12 @@ public function test_parentRelationship() { $this->assertEquals('Mighty Sword', $knight->sword->name); } - public function test_satelliteRelationship() { + /** + * test_satelliteRelationship. + * + * @return void + */ + public function test_satelliteRelationship(): void { \vhs\database\Database::setEngine(self::$inMemoryEngine); $enchantment = new \tests\domain\Enchantment(); @@ -213,6 +208,7 @@ public function test_satelliteRelationship() { unset($enchantment, $sword); + /** @var \tests\domain\Sword */ $sword = \tests\domain\Sword::find($swordid); $enchants = $sword->enchantments->all(); @@ -225,6 +221,11 @@ public function test_satelliteRelationship() { $enchants[$enchantmentid]->delete(); } + /** + * setUpBeforeClass. + * + * @return void + */ public static function setUpBeforeClass(): void { self::$logger = new \vhs\loggers\ConsoleLogger(); self::$inMemoryEngine = new \vhs\database\engines\memory\InMemoryEngine(); @@ -248,15 +249,30 @@ public static function setUpBeforeClass(): void { \vhs\database\Database::setEngine(self::$inMemoryEngine); } + /** + * tearDownAfterClass. + * + * @return void + */ public static function tearDownAfterClass(): void { //\vhs\database\Database::setEngine(self::$mySqlEngine); //\vhs\database\Database::arbitrary("DROP DATABASE " . DB_DATABASE); } + /** + * setUp. + * + * @return void + */ protected function setUp(): void { } + /** + * tearDown. + * + * @return void + */ protected function tearDown(): void { } } diff --git a/tests/EmailTemplateDomainTest.php b/packages/backend-php/tests/EmailTemplateDomainTest.php similarity index 83% rename from tests/EmailTemplateDomainTest.php rename to packages/backend-php/tests/EmailTemplateDomainTest.php index 0814088e9..e41cee496 100644 --- a/tests/EmailTemplateDomainTest.php +++ b/packages/backend-php/tests/EmailTemplateDomainTest.php @@ -20,12 +20,22 @@ class EmailTemplateDomainTest extends TestCase { /** @var Logger */ private static $logger; - private $ids = []; - - public function test_Service() { + /** + * ids. + * + * @ var array + */ + // private $ids = []; + + /** + * test_Service. + * + * @return void + */ + public function test_Service(): void { $service = new EmailService(); - $rows = $service->ListTemplates(1, 1, 'id', 'id', ''); + $rows = array_map(fn ($row): array => get_object_vars($row), array: json_decode(json_encode($service->ListTemplates(1, 1, 'id', 'id', '')))); $this->assertCount(1, $rows); @@ -88,7 +98,7 @@ public function test_Service() { $service->PutTemplate('qwer', 'qwer', 'qwer', 'qwer', 'qwer', 'qwer'); - $rows = $service->ListTemplates(1, 1, 'id', 'id', ''); + $rows = array_map(fn ($row): array => get_object_vars($row), json_decode(json_encode($service->ListTemplates(1, 1, 'id', 'id', '')))); $this->assertCount(1, $rows); @@ -108,18 +118,28 @@ public function test_Service() { $this->assertEquals('qwer', $template->html); } - public function test_Template() { + /** + * test_Template. + * + * @return void + */ + public function test_Template(): void { $generated = EmailTemplate::generate('some_random_name', [ 'a' => 'the value for a', 'other_value' => 'some other value', 'random' => 'random' ]); - $this->assertEquals('the value for a some other value asdf', $generated['subject']); - $this->assertEquals('the value for a some other value qwer', $generated['txt']); - $this->assertEquals('the value for a some other value zxcv', $generated['html']); + $this->assertEquals('the value for a some other value asdf', $generated->subject); + $this->assertEquals('the value for a some other value qwer', $generated->txt); + $this->assertEquals('the value for a some other value zxcv', $generated->html); } + /** + * setUpBeforeClass. + * + * @return void + */ public static function setUpBeforeClass(): void { self::$logger = new ConsoleLogger(); self::$engine = new InMemoryEngine(); @@ -129,21 +149,38 @@ public static function setUpBeforeClass(): void { Database::setRethrow(true); } + /** + * tearDownAfterClass. + * + * @return void + */ public static function tearDownAfterClass(): void { self::$engine->disconnect(); } + /** + * setUp. + * + * @return void + */ public function setUp(): void { $template = new EmailTemplate(); + $template->name = 'This is the most random template, srsly'; $template->code = 'some_random_name'; $template->subject = '{{a}} {{other_value}} asdf'; $template->help = 'some help text to describe whatever this is'; $template->body = '{{a}} {{other_value}} qwer'; $template->html = '{{a}} {{other_value}} zxcv'; + $template->save(); } + /** + * tearDown. + * + * @return void + */ public function tearDown(): void { self::$engine->disconnect(); } diff --git a/tests/KeyDomainTest.php b/packages/backend-php/tests/KeyDomainTest.php similarity index 87% rename from tests/KeyDomainTest.php rename to packages/backend-php/tests/KeyDomainTest.php index 05b21533e..d3d9e96c3 100644 --- a/tests/KeyDomainTest.php +++ b/packages/backend-php/tests/KeyDomainTest.php @@ -4,6 +4,7 @@ use app\domain\Membership; use app\domain\Privilege; use app\domain\User; +use app\dto\UserActiveEnum; use app\services\AuthService; use PHPUnit\Framework\TestCase; use vhs\database\Database; @@ -20,12 +21,23 @@ class KeyDomainTest extends TestCase { /** @var InMemoryEngine */ private static $engine; + /** @var Logger */ private static $logger; + /** + * ids. + * + * @var array + */ private $ids = []; - public function test_bullshitPhp() { + /** + * test_bullshitPhp. + * + * @return void + */ + public function test_bullshitPhp(): void { $service = new AuthService(); $result = $service->CheckPin('00011234'); @@ -50,9 +62,19 @@ public function test_bullshitPhp() { $this->assertTrue(is_array($obj->privileges), 'privileges must be an array'); } - public function test_Privileges() { + /** + * test_Privileges. + * + * @return void + */ + public function test_Privileges(): void { + /** @var \app\domain\Privilege */ $inherit = Privilege::find($this->ids['inherit']); + + /** @var \app\domain\Privilege */ $membership_privilege = Privilege::find($this->ids['membership_privilege']); + + /** @var \app\domain\Privilege */ $user_privilege = Privilege::find($this->ids['user_privilege']); $service = new AuthService(); @@ -92,6 +114,11 @@ public function test_Privileges() { $this->assertTrue($user_privilegeFound); } + /** + * setUpBeforeClass. + * + * @return void + */ public static function setUpBeforeClass(): void { self::$logger = new ConsoleLogger(); self::$engine = new InMemoryEngine(); @@ -101,15 +128,27 @@ public static function setUpBeforeClass(): void { Database::setRethrow(true); } + /** + * tearDownAfterClass. + * + * @return void + */ public static function tearDownAfterClass(): void { self::$engine->disconnect(); } + /** + * setUp. + * + * @return void + */ public function setUp(): void { $inherit = new Privilege(); + $inherit->name = 'Inherit Privilege'; $inherit->code = 'inherit'; $inherit->enabled = true; + $inherit->save(); $this->ids['inherit'] = $inherit->id; @@ -143,7 +182,7 @@ public function setUp(): void { $user->membership = $membership; $user->username = 'vbnm'; $user->email = 'nomos_tests@vanhack.ca'; - $user->active = 'y'; + $user->active = UserActiveEnum::ACTIVE->value; $user->privileges->add($user_privilege); $user_expiry = new DateTime('today'); $user_expiry->modify('+1 month'); @@ -162,6 +201,11 @@ public function setUp(): void { $this->ids['key'] = $key->id; } + /** + * tearDown. + * + * @return void + */ public function tearDown(): void { self::$engine->disconnect(); } diff --git a/tests/LimitTest.php b/packages/backend-php/tests/LimitTest.php similarity index 74% rename from tests/LimitTest.php rename to packages/backend-php/tests/LimitTest.php index 718e5c439..7e28d2ad6 100644 --- a/tests/LimitTest.php +++ b/packages/backend-php/tests/LimitTest.php @@ -11,20 +11,34 @@ use vhs\database\Database; use vhs\database\limits\Limit; use vhs\database\offsets\Offset; -use vhs\database\Table; -use vhs\database\types\Type; -use vhs\domain\Schema; use vhs\Logger; use vhs\loggers\ConsoleLogger; +/** @typescript */ class LimitTest extends TestCase { /** @var Logger */ private static $logger; + + /** + * inMemoryGenerator. + * + * @var mixed + */ private $inMemoryGenerator; + /** + * mySqlGenerator. + * + * @var mixed + */ private $mySqlGenerator; - public function test_EmptyLimit() { + /** + * test_EmptyLimit. + * + * @return void + */ + public function test_EmptyLimit(): void { $limit = Limit::Limit(null); $clause = $limit->generate($this->mySqlGenerator); @@ -36,7 +50,12 @@ public function test_EmptyLimit() { $this->assertEquals('', $clause); } - public function test_EmptyOffset() { + /** + * test_EmptyOffset. + * + * @return void + */ + public function test_EmptyOffset(): void { $offset = Offset::Offset(null); $clause = $offset->generate($this->mySqlGenerator); @@ -48,6 +67,11 @@ public function test_EmptyOffset() { $this->assertEquals('', $clause); } + /** + * test_HasLimit. + * + * @return void + */ public function test_HasLimit() { $limit = Limit::Limit(1); @@ -60,6 +84,11 @@ public function test_HasLimit() { $this->assertEquals('1', $clause); } + /** + * test_HasOffset. + * + * @return void + */ public function test_HasOffset() { $offset = Offset::Offset(10); @@ -72,6 +101,11 @@ public function test_HasOffset() { $this->assertEquals('10', $clause); } + /** + * setUpBeforeClass. + * + * @return void + */ public static function setUpBeforeClass(): void { self::$logger = new ConsoleLogger(); Database::setLogger(self::$logger); @@ -79,14 +113,29 @@ public static function setUpBeforeClass(): void { Database::setRethrow(true); } + /** + * tearDownAfterClass. + * + * @return void + */ public static function tearDownAfterClass(): void { } + /** + * setUp. + * + * @return void + */ public function setUp(): void { $this->mySqlGenerator = new \vhs\database\engines\mysql\MySqlGenerator(); $this->inMemoryGenerator = new \vhs\database\engines\memory\InMemoryGenerator(); } + /** + * tearDown. + * + * @return void + */ public function tearDown(): void { } } diff --git a/tests/ServiceTest.php b/packages/backend-php/tests/ServiceTest.php similarity index 70% rename from tests/ServiceTest.php rename to packages/backend-php/tests/ServiceTest.php index 3e5251a09..bd084d7b2 100644 --- a/tests/ServiceTest.php +++ b/packages/backend-php/tests/ServiceTest.php @@ -8,214 +8,319 @@ */ use PHPUnit\Framework\TestCase; +use tests\security\PermPrincipal; use vhs\services\ServiceClient; -use vhs\services\ServiceHandler; use vhs\services\ServiceRegistry; -class PermPrincipal implements \vhs\security\IPrincipal { - private $perms; - - public function __construct(...$perms) { - if (is_null($perms)) { - $perms = []; - } - - $this->perms = $perms; - } - - public function canGrantAllPermissions(...$permission) { - // TODO: Implement canGrantAllPermissions() method. - } - - public function canGrantAnyPermissions(...$permission) { - // TODO: Implement canGrantAnyPermissions() method. - } - - public function getIdentity() { - return null; - } - - public function hasAllPermissions(...$permission) { - return count(array_diff($permission, $this->perms)) == 0; - } - - public function hasAnyPermissions(...$permission) { - return count(array_intersect($permission, $this->perms)) > 0; - } - - public function isAnon() { - return false; - } - - public function __toString() { - return 'perm'; - } -} - +/** @typescript */ class ServiceTest extends TestCase { + /** + * test_AllPermMethod. + * + * @return void + */ public function test_AllPermMethod() { \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1', 'perm2', 'perm3')); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('AllPermMethod!', ServiceClient::web_TestService1_AllPermMethod()); } + /** + * test_AllPermMethod_authedOnly. + * + * @return void + */ public function test_AllPermMethod_authedOnly() { $this->expectException(\Exception::class); $this->expectExceptionMessage('Access denied'); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal()); + + // @phpstan-ignore staticMethod.notFound ServiceClient::web_TestService1_AllPermMethod(); } + /** + * test_AllPermMethod_missingPerm2. + * + * @return void + */ public function test_AllPermMethod_missingPerm2() { $this->expectException(\Exception::class); $this->expectExceptionMessage('Access denied'); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1', 'perm3')); + + // @phpstan-ignore staticMethod.notFound ServiceClient::web_TestService1_AllPermMethod(); } + /** + * test_AllPermMethod_missingPerm3. + * + * @return void + */ public function test_AllPermMethod_missingPerm3() { $this->expectException(\Exception::class); $this->expectExceptionMessage('Access denied'); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1', 'perm2')); + + // @phpstan-ignore staticMethod.notFound ServiceClient::web_TestService1_AllPermMethod(); } + /** + * test_AnonMethod_asAnon. + * + * @return void + */ public function test_AnonMethod_asAnon() { $this->assertTrue(\vhs\security\CurrentUser::getPrincipal()->isAnon()); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('AnonMethod!', ServiceClient::web_TestService1_AnonMethod()); + // @phpstan-ignore staticMethod.notFound $this->assertEquals('AnonMethod!', ServiceClient::web_TestService1_AnonMethod()); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal()); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('AnonMethod!', ServiceClient::web_TestService1_AnonMethod()); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('randomPermission')); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('AnonMethod!', ServiceClient::web_TestService1_AnonMethod()); } + /** + * test_AnyPermMethod. + * + * @return void + */ public function test_AnyPermMethod() { \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1')); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('AnyPermMethod!', ServiceClient::web_TestService1_AnyPermMethod()); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm2')); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('AnyPermMethod!', ServiceClient::web_TestService1_AnyPermMethod()); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm3')); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('AnyPermMethod!', ServiceClient::web_TestService1_AnyPermMethod()); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1', 'perm2')); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('AnyPermMethod!', ServiceClient::web_TestService1_AnyPermMethod()); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm2', 'perm3')); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('AnyPermMethod!', ServiceClient::web_TestService1_AnyPermMethod()); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1', 'perm2', 'perm3')); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('AnyPermMethod!', ServiceClient::web_TestService1_AnyPermMethod()); } + /** + * test_AnyPermMethod_authedOnly. + * + * @return void + */ public function test_AnyPermMethod_authedOnly() { $this->expectException(\Exception::class); $this->expectExceptionMessage('Access denied'); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal()); + + // @phpstan-ignore staticMethod.notFound ServiceClient::web_TestService1_AnyPermMethod(); } + /** + * test_AnyPermMethod_wrongSet. + * + * @return void + */ public function test_AnyPermMethod_wrongSet() { $this->expectException(\Exception::class); $this->expectExceptionMessage('Access denied'); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('asdf', 'zxcv')); + + // @phpstan-ignore staticMethod.notFound ServiceClient::web_TestService1_AnyPermMethod(); } + /** + * test_ArgMethod_asAnon. + * + * @return void + */ public function test_ArgMethod_asAnon() { - //$data = '{ "a": "hello ", "b": "world" }'; + // $data = '{ "a": "hello ", "b": "world" }'; + // @phpstan-ignore staticMethod.notFound $this->assertEquals('ArgMethod: hello world', ServiceClient::web_TestService1_ArgMethod('hello ', 'world')); } + /** + * test_AuthMethod_asAnon. + * + * @return void + */ public function test_AuthMethod_asAnon() { $this->expectException(\Exception::class); $this->expectExceptionMessage('Access denied'); $this->assertTrue(\vhs\security\CurrentUser::getPrincipal()->isAnon()); + + // @phpstan-ignore staticMethod.notFound ServiceClient::web_TestService1_AuthMethod(); } + /** + * test_AuthMethod_asAuth. + * + * @return void + */ public function test_AuthMethod_asAuth() { \vhs\security\CurrentUser::setPrincipal(new PermPrincipal()); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('AuthMethod!', ServiceClient::web_TestService1_AuthMethod()); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('randomPermission')); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('AuthMethod!', ServiceClient::web_TestService1_AuthMethod()); } + /** + * test_EmptyPermMethod_asAnon. + * + * @return void + */ public function test_EmptyPermMethod_asAnon() { $this->expectException(\Exception::class); $this->expectExceptionMessage('Service contract method requires permission context.'); $this->assertTrue(\vhs\security\CurrentUser::getPrincipal()->isAnon()); + // @phpstan-ignore staticMethod.notFound ServiceClient::web_TestService1_EmptyPermMethod(); } + /** + * test_MissingPermMethod_asAnon. + * + * @return void + */ public function test_MissingPermMethod_asAnon() { $this->expectException(\Exception::class); $this->expectExceptionMessage('Service contract method requires permission context.'); $this->assertTrue(\vhs\security\CurrentUser::getPrincipal()->isAnon()); + // @phpstan-ignore staticMethod.notFound ServiceClient::web_TestService1_MissingPermMethod(); } + /** + * test_MultiPermMethod. + * + * @return void + */ public function test_MultiPermMethod() { \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1', 'perm2')); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('MultiPermMethod!', ServiceClient::web_TestService1_MultiPermMethod()); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm2', 'perm3')); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('MultiPermMethod!', ServiceClient::web_TestService1_MultiPermMethod()); \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1', 'perm2', 'perm3')); + + // @phpstan-ignore staticMethod.notFound $this->assertEquals('MultiPermMethod!', ServiceClient::web_TestService1_MultiPermMethod()); } + /** + * test_native_ArgMethod_asAnon. + * + * @return void + */ public function test_native_ArgMethod_asAnon() { $data = [ 'a' => 'hello ', 'b' => 'world' ]; + // @phpstan-ignore staticMethod.notFound $this->assertEquals('ArgMethod: hello world', ServiceClient::native_TestService1_ArgMethod('hello ', 'world')); //ServiceRegistry::get("native")->handle("/services/native/TestService1.svc/ArgMethod", $data)); } + /** + * test_NoDocMethod_asAnon. + * + * @return void + */ public function test_NoDocMethod_asAnon() { $this->expectException(\Exception::class); $this->expectExceptionMessage('Service contract method requires permission context.'); $this->assertTrue(\vhs\security\CurrentUser::getPrincipal()->isAnon()); + // @phpstan-ignore staticMethod.notFound ServiceClient::web_TestService1_NoDocMethod(); } + /** + * test_ObjReturnMethod_asAnon. + * + * @return void + */ public function test_ObjReturnMethod_asAnon() { $data = '{ "a": "hello " }'; $this->assertEquals('{"retA":"hello "}', ServiceRegistry::get('web')->handle('/services/web/TestService1.svc/ObjReturnMethod', $data)); } + /** + * test_PermMethod_asAnon. + * + * @return void + */ public function test_PermMethod_asAnon() { $this->expectException(\Exception::class); $this->expectExceptionMessage('Access denied'); $this->assertTrue(\vhs\security\CurrentUser::getPrincipal()->isAnon()); + // @phpstan-ignore staticMethod.notFound ServiceClient::web_TestService1_PermMethod(); } + /** + * test_PermMethod_asAuth. + * + * @return void + */ public function test_PermMethod_asAuth() { $this->expectException(\Exception::class); $this->expectExceptionMessage('Access denied'); @@ -224,27 +329,55 @@ public function test_PermMethod_asAuth() { $this->assertFalse(\vhs\security\CurrentUser::getPrincipal()->isAnon()); + // @phpstan-ignore staticMethod.notFound ServiceClient::web_TestService1_PermMethod(); } + /** + * test_PermMethod_asPerm. + * + * @return void + */ public function test_PermMethod_asPerm() { \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('randomPermission')); + // @phpstan-ignore staticMethod.notFound $this->assertEquals('PermMethod!', ServiceClient::web_TestService1_PermMethod()); } + /** + * setUpBeforeClass. + * + * @return void + */ public static function setUpBeforeClass(): void { $logger = new \vhs\loggers\SilentLogger(); - ServiceRegistry::register($logger, 'web', 'tests\\endpoints\\web', dirname(__FILE__) . '/..'); - ServiceRegistry::register($logger, 'native', 'tests\\endpoints\\native', dirname(__FILE__) . '/..'); + + ServiceRegistry::register($logger, 'web', 'tests\\endpoints\\web', \vhs\BasePath::getBasePath(false)); + ServiceRegistry::register($logger, 'native', 'tests\\endpoints\\native', \vhs\BasePath::getBasePath(false)); } + /** + * tearDownAfterClass. + * + * @return void + */ public static function tearDownAfterClass(): void { } + /** + * setUp. + * + * @return void + */ protected function setUp(): void { } + /** + * tearDown. + * + * @return void + */ protected function tearDown(): void { \vhs\security\CurrentUser::setPrincipal(new \vhs\security\AnonPrincipal()); } diff --git a/tests/WhereTest.php b/packages/backend-php/tests/WhereTest.php similarity index 84% rename from tests/WhereTest.php rename to packages/backend-php/tests/WhereTest.php index 10c6d243a..1d904d424 100644 --- a/tests/WhereTest.php +++ b/packages/backend-php/tests/WhereTest.php @@ -16,6 +16,7 @@ use vhs\Logger; use vhs\loggers\ConsoleLogger; +/** @typescript */ class TestSchema extends Schema { /** * @return Table @@ -30,14 +31,31 @@ public static function init() { } } +/** @typescript */ class WhereTest extends TestCase { /** @var Logger */ private static $logger; + + /** + * inMemoryGenerator. + * + * @var mixed + */ private $inMemoryGenerator; + /** + * mySqlGenerator. + * + * @var mixed + */ private $mySqlGenerator; - public function test_And() { + /** + * test_And. + * + * @return void + */ + public function test_And(): void { $where = Where::_And(Where::Equal(TestSchema::Column('test1'), 'test2'), Where::Equal(TestSchema::Column('test3'), 'test4')); $clause = $where->generate($this->mySqlGenerator); @@ -61,7 +79,12 @@ public function test_And() { $this->assertFalse($clause($data2)); } - public function test_AndOr() { + /** + * test_AndOr. + * + * @return void + */ + public function test_AndOr(): void { $where = Where::_And( Where::Equal(TestSchema::Column('test1'), 'test2'), Where::_Or(Where::Equal(TestSchema::Column('test3'), 'test4'), Where::Equal(TestSchema::Column('test5'), 'test6')) @@ -111,7 +134,12 @@ public function test_AndOr() { $this->assertFalse($clause($data5)); } - public function test_Equal() { + /** + * test_Equal. + * + * @return void + */ + public function test_Equal(): void { $where = Where::Equal(TestSchema::Column('test1'), 'test2'); $clause = $where->generate($this->mySqlGenerator); @@ -128,7 +156,12 @@ public function test_Equal() { $this->assertFalse($clause($data2)); } - public function test_Greater() { + /** + * test_Greater. + * + * @return void + */ + public function test_Greater(): void { $where = Where::Greater(TestSchema::Column('test1'), 1); $clause = $where->generate($this->mySqlGenerator); @@ -145,7 +178,12 @@ public function test_Greater() { $this->assertTrue($clause($data2)); } - public function test_GreaterEqual() { + /** + * test_GreaterEqual. + * + * @return void + */ + public function test_GreaterEqual(): void { $where = Where::GreaterEqual(TestSchema::Column('test1'), 1); $clause = $where->generate($this->mySqlGenerator); @@ -164,7 +202,12 @@ public function test_GreaterEqual() { $this->assertTrue($clause($data3)); } - public function test_In() { + /** + * test_In. + * + * @return void + */ + public function test_In(): void { $where = Where::In(TestSchema::Column('test1'), ['a', 'b', 'c']); $clause = $where->generate($this->mySqlGenerator); @@ -185,7 +228,12 @@ public function test_In() { $this->assertFalse($clause($data4)); } - public function test_Lesser() { + /** + * test_Lesser. + * + * @return void + */ + public function test_Lesser(): void { $where = Where::Lesser(TestSchema::Column('test1'), 2); $clause = $where->generate($this->mySqlGenerator); @@ -202,7 +250,12 @@ public function test_Lesser() { $this->assertFalse($clause($data2)); } - public function test_LesserEqual() { + /** + * test_LesserEqual. + * + * @return void + */ + public function test_LesserEqual(): void { $where = Where::LesserEqual(TestSchema::Column('test1'), 2); $clause = $where->generate($this->mySqlGenerator); @@ -221,7 +274,12 @@ public function test_LesserEqual() { $this->assertFalse($clause($data3)); } - public function test_NotEqual() { + /** + * test_NotEqual. + * + * @return void + */ + public function test_NotEqual(): void { $where = Where::NotEqual(TestSchema::Column('test1'), 'test2'); $clause = $where->generate($this->mySqlGenerator); @@ -238,7 +296,12 @@ public function test_NotEqual() { $this->assertTrue($clause($data2)); } - public function test_NotIn() { + /** + * test_NotIn. + * + * @return void + */ + public function test_NotIn(): void { $where = Where::NotIn(TestSchema::Column('test1'), ['a', 'b', 'c']); $clause = $where->generate($this->mySqlGenerator); @@ -259,7 +322,12 @@ public function test_NotIn() { $this->assertTrue($clause($data4)); } - public function test_NotNull() { + /** + * test_NotNull. + * + * @return void + */ + public function test_NotNull(): void { $where = Where::NotNull(TestSchema::Column('test1')); $clause = $where->generate($this->mySqlGenerator); @@ -276,7 +344,12 @@ public function test_NotNull() { $this->assertTrue($clause($data2)); } - public function test_Null() { + /** + * test_Null. + * + * @return void + */ + public function test_Null(): void { $where = Where::Null(TestSchema::Column('test1')); $clause = $where->generate($this->mySqlGenerator); @@ -293,7 +366,12 @@ public function test_Null() { $this->assertFalse($clause($data2), "failed by matching 'test3' as null"); } - public function test_Or() { + /** + * test_Or. + * + * @return void + */ + public function test_Or(): void { $where = Where::_Or(Where::Equal(TestSchema::Column('test1'), 'test2'), Where::Equal(TestSchema::Column('test3'), 'test4')); $clause = $where->generate($this->mySqlGenerator); @@ -323,6 +401,11 @@ public function test_Or() { $this->assertFalse($clause($data3)); } + /** + * setUpBeforeClass. + * + * @return void + */ public static function setUpBeforeClass(): void { self::$logger = new ConsoleLogger(); Database::setLogger(self::$logger); @@ -330,14 +413,29 @@ public static function setUpBeforeClass(): void { Database::setRethrow(true); } + /** + * tearDownAfterClass. + * + * @return void + */ public static function tearDownAfterClass(): void { } + /** + * setUp. + * + * @return void + */ public function setUp(): void { $this->mySqlGenerator = new \vhs\database\engines\mysql\MySqlGenerator(); $this->inMemoryGenerator = new \vhs\database\engines\memory\InMemoryGenerator(); } + /** + * tearDown. + * + * @return void + */ public function tearDown(): void { } } diff --git a/tests/autoload.php b/packages/backend-php/tests/autoload.php similarity index 70% rename from tests/autoload.php rename to packages/backend-php/tests/autoload.php index e472e947e..52aa00f67 100644 --- a/tests/autoload.php +++ b/packages/backend-php/tests/autoload.php @@ -10,5 +10,7 @@ require_once 'vendor/autoload.php'; require_once 'vhs/vhs.php'; -\vhs\SplClassLoader::getInstance()->add(new \vhs\SplClassLoaderItem('tests', dirname(__FILE__) . '/..')); -\vhs\SplClassLoader::getInstance()->add(new \vhs\SplClassLoaderItem('app', dirname(__FILE__) . '/..')); +define('DEBUG', false); + +\vhs\SplClassLoader::getInstance()->add(new \vhs\SplClassLoaderItem('tests', \vhs\BasePath::getBasePath(false))); +\vhs\SplClassLoader::getInstance()->add(new \vhs\SplClassLoaderItem('app', \vhs\BasePath::getBasePath(false))); diff --git a/tests/contracts/ITestService1.php b/packages/backend-php/tests/contracts/ITestService1.php similarity index 88% rename from tests/contracts/ITestService1.php rename to packages/backend-php/tests/contracts/ITestService1.php index a1d39adcf..88f1efd99 100644 --- a/tests/contracts/ITestService1.php +++ b/packages/backend-php/tests/contracts/ITestService1.php @@ -38,8 +38,8 @@ public function AnyPermMethod(); /** * @permission anonymous * - * @param $a - * @param $b + * @param mixed $a + * @param mixed $b * * @return mixed */ @@ -72,12 +72,17 @@ public function MissingPermMethod(); */ public function MultiPermMethod(); - public function NoDocMethod(); + /** + * NoDocMethod. + * + * @return string + */ + public function NoDocMethod(): string; /** * @permission anonymous * - * @param $a + * @param mixed $a * * @return mixed */ diff --git a/tests/domain/Enchantment.php b/packages/backend-php/tests/domain/Enchantment.php similarity index 73% rename from tests/domain/Enchantment.php rename to packages/backend-php/tests/domain/Enchantment.php index b10982ee9..589cd647d 100644 --- a/tests/domain/Enchantment.php +++ b/packages/backend-php/tests/domain/Enchantment.php @@ -13,8 +13,22 @@ use vhs\domain\Domain; use vhs\domain\validations\ValidationResults; +/** + * @property int $id + * @property string $name + * @property float $bonus + * + * @extends Domain + * + * @typescript + */ class Enchantment extends Domain { - public static function Define() { + /** + * Define. + * + * @return void + */ + public static function Define(): void { Enchantment::Schema(EnchantmentSchema::Type()); //NOTE don't setup the same relationships on the child of a previously defined parent, this will cause a hydrate loop. @@ -24,7 +38,7 @@ public static function Define() { /** * @param ValidationResults $results * - * @return bool + * @return void */ public function validate(ValidationResults &$results) { // TODO: Implement validate() method. diff --git a/packages/backend-php/tests/domain/ExampleDomain.php b/packages/backend-php/tests/domain/ExampleDomain.php new file mode 100644 index 000000000..cc1272e66 --- /dev/null +++ b/packages/backend-php/tests/domain/ExampleDomain.php @@ -0,0 +1,84 @@ + + * + * @typescript + */ +class ExampleDomain extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + ExampleDomain::Schema(ExampleSchema::Type()); + } + + /** + * get_magic. + * + * @return string + */ + public function get_magic(): string { + return 'magic field'; + } + + /** + * get_testC. + * + * @return string + */ + public function get_testC(): string { + return $this->internal_testC . 'fail'; + } + + /** + * set_magic. + * + * @param mixed $value + * + * @return void + */ + public function set_magic($value) { + $this->testC = $value . 'magic'; + } + + /** + * set_testC. + * + * @param mixed $value + * + * @return void + */ + public function set_testC($value) { + $this->internal_testC = $value . 'pass'; + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + if ($this->testA != 'pass') { + $results->add(new ValidationFailure('testA is not equal to pass')); + } + } +} diff --git a/tests/domain/Knight.php b/packages/backend-php/tests/domain/Knight.php similarity index 59% rename from tests/domain/Knight.php rename to packages/backend-php/tests/domain/Knight.php index 689a03612..b3a8b9055 100644 --- a/tests/domain/Knight.php +++ b/packages/backend-php/tests/domain/Knight.php @@ -13,8 +13,25 @@ use vhs\domain\Domain; use vhs\domain\validations\ValidationResults; +/** + * @property int|\vhs\database\Column $id + * @property int $swordid + * @property string $name + * @property string $birthdate + * @property object $sword + * @property object $rings + * + * @extends Domain + * + * @typescript + */ class Knight extends Domain { - public static function Define() { + /** + * Define. + * + * @return void + */ + public static function Define(): void { Knight::Schema(KnightSchema::Type()); Knight::Relationship('sword', Sword::Type()); //parent relationship aka Many to One Knight::Relationship('rings', Ring::Type()); //child relationship aka One to Many @@ -23,7 +40,7 @@ public static function Define() { /** * @param ValidationResults $results * - * @return bool + * @return void */ public function validate(ValidationResults &$results) { // TODO: Implement validate() method. diff --git a/tests/domain/Ring.php b/packages/backend-php/tests/domain/Ring.php similarity index 55% rename from tests/domain/Ring.php rename to packages/backend-php/tests/domain/Ring.php index 71240f651..f335c6d24 100644 --- a/tests/domain/Ring.php +++ b/packages/backend-php/tests/domain/Ring.php @@ -13,8 +13,25 @@ use vhs\domain\Domain; use vhs\domain\validations\ValidationResults; +/** + * @property int $id + * @property string $name + * @property int $knightid + * @property int $enchantmentid + * @property \tests\domain\Knight $knight + * @property \tests\domain\Enchantment $enchantment + * + * @extends Domain + * + * @typescript + */ class Ring extends Domain { - public static function Define() { + /** + * Define. + * + * @return void + */ + public static function Define(): void { Ring::Schema(RingSchema::Type()); Ring::Relationship('enchantment', Enchantment::Type()); //parent relationship Many to One @@ -23,7 +40,7 @@ public static function Define() { /** * @param ValidationResults $results * - * @return bool + * @return void */ public function validate(ValidationResults &$results) { // TODO: Implement validate() method. diff --git a/tests/domain/Sword.php b/packages/backend-php/tests/domain/Sword.php similarity index 73% rename from tests/domain/Sword.php rename to packages/backend-php/tests/domain/Sword.php index f5c61eb84..103ef49e7 100644 --- a/tests/domain/Sword.php +++ b/packages/backend-php/tests/domain/Sword.php @@ -14,8 +14,23 @@ use vhs\domain\Domain; use vhs\domain\validations\ValidationResults; +/** + * @property int $id + * @property string $name + * @property int $damage + * @property object $enchantments + * + * @extends Domain + * + * @typescript + */ class Sword extends Domain { - public static function Define() { + /** + * Define. + * + * @return void + */ + public static function Define(): void { Sword::Schema(SwordSchema::Type()); //NOTE don't setup the same relationships on the child of a previously defined parent, this will cause a hydrate loop. @@ -25,7 +40,7 @@ public static function Define() { /** * @param ValidationResults $results * - * @return bool + * @return void */ public function validate(ValidationResults &$results) { // TODO: Implement validate() method. diff --git a/tests/endpoints/native/TestService1.svc.php b/packages/backend-php/tests/endpoints/native/TestService1.svc.php similarity index 95% rename from tests/endpoints/native/TestService1.svc.php rename to packages/backend-php/tests/endpoints/native/TestService1.svc.php index f1e89ea35..9c294dff1 100644 --- a/tests/endpoints/native/TestService1.svc.php +++ b/packages/backend-php/tests/endpoints/native/TestService1.svc.php @@ -13,6 +13,7 @@ use vhs\services\endpoints\NativeEndpoint; use vhs\services\ServiceContext; +/** @typescript */ class TestService1 extends NativeEndpoint { public function __construct() { parent::__construct(new TestService(new ServiceContext($this))); diff --git a/tests/endpoints/web/TestService1.svc.php b/packages/backend-php/tests/endpoints/web/TestService1.svc.php similarity index 95% rename from tests/endpoints/web/TestService1.svc.php rename to packages/backend-php/tests/endpoints/web/TestService1.svc.php index 9331a8bab..95bbda7e9 100644 --- a/tests/endpoints/web/TestService1.svc.php +++ b/packages/backend-php/tests/endpoints/web/TestService1.svc.php @@ -13,6 +13,7 @@ use vhs\services\endpoints\JsonEndpoint; use vhs\services\ServiceContext; +/** @typescript */ class TestService1 extends JsonEndpoint { public function __construct() { parent::__construct(new TestService(new ServiceContext($this))); diff --git a/tests/schema/EnchantmentSchema.php b/packages/backend-php/tests/schema/EnchantmentSchema.php similarity index 87% rename from tests/schema/EnchantmentSchema.php rename to packages/backend-php/tests/schema/EnchantmentSchema.php index 132eabe32..3efa9f28c 100644 --- a/tests/schema/EnchantmentSchema.php +++ b/packages/backend-php/tests/schema/EnchantmentSchema.php @@ -14,6 +14,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class EnchantmentSchema extends Schema { /** * @return Table @@ -25,6 +26,8 @@ public static function init() { $table->addColumn('name', Type::String(false, 'Mystery Enchament', 50)); $table->addColumn('bonus', Type::Float(false, 1.1)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); return $table; diff --git a/packages/backend-php/tests/schema/ExampleSchema.php b/packages/backend-php/tests/schema/ExampleSchema.php new file mode 100644 index 000000000..c0b436b12 --- /dev/null +++ b/packages/backend-php/tests/schema/ExampleSchema.php @@ -0,0 +1,33 @@ +addColumn('id', Type::Int()); + $table->addColumn('testA', Type::String(true)); + $table->addColumn('testB', Type::String(true)); + $table->addColumn('testC', Type::String(true)); + + // TODO fix proper typing + // @phpstan-ignore property.notFound + $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); + + return $table; + } +} diff --git a/tests/schema/KnightSchema.php b/packages/backend-php/tests/schema/KnightSchema.php similarity index 54% rename from tests/schema/KnightSchema.php rename to packages/backend-php/tests/schema/KnightSchema.php index 9e63eab84..e99b7c678 100644 --- a/tests/schema/KnightSchema.php +++ b/packages/backend-php/tests/schema/KnightSchema.php @@ -14,6 +14,11 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** + * @method static \tests\domain\Knight Columns() + * + * @typescript + */ class KnightSchema extends Schema { /** * @return Table @@ -26,8 +31,20 @@ public static function init() { $table->addColumn('birthdate', Type::DateTime()); $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->swordid, SwordSchema::Table(), SwordSchema::Columns()->id) + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->swordid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + SwordSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + SwordSchema::Columns()->id + ) ); return $table; diff --git a/packages/backend-php/tests/schema/RingSchema.php b/packages/backend-php/tests/schema/RingSchema.php new file mode 100644 index 000000000..ab761f629 --- /dev/null +++ b/packages/backend-php/tests/schema/RingSchema.php @@ -0,0 +1,58 @@ +addColumn('id', Type::Int(false, 0)); + $table->addColumn('name', Type::String(false, 'Mystery Ring', 50)); + $table->addColumn('knightid', Type::Int(false, 0)); + $table->addColumn('enchantmentid', Type::Int(false, 0)); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->id), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->enchantmentid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + EnchantmentSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + EnchantmentSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->knightid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + KnightSchema::Table(), + KnightSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/packages/backend-php/tests/schema/SwordEnchantmentsSchema.php b/packages/backend-php/tests/schema/SwordEnchantmentsSchema.php new file mode 100644 index 000000000..1525864f0 --- /dev/null +++ b/packages/backend-php/tests/schema/SwordEnchantmentsSchema.php @@ -0,0 +1,61 @@ +addColumn('swordid', Type::Int(false, 0)); + $table->addColumn('enchantmentid', Type::Int(false, 0)); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->swordid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->enchantmentid), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->swordid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + SwordSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + SwordSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->enchantmentid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + EnchantmentSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + EnchantmentSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/tests/schema/SwordSchema.php b/packages/backend-php/tests/schema/SwordSchema.php similarity index 86% rename from tests/schema/SwordSchema.php rename to packages/backend-php/tests/schema/SwordSchema.php index 8a0575bd1..ff1aa8083 100644 --- a/tests/schema/SwordSchema.php +++ b/packages/backend-php/tests/schema/SwordSchema.php @@ -14,6 +14,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class SwordSchema extends Schema { /** * @return Table @@ -25,6 +26,8 @@ public static function init() { $table->addColumn('name', Type::String(false, 'Mystery Sword', 50)); $table->addColumn('damage', Type::Int(false, 5)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); return $table; diff --git a/packages/backend-php/tests/security/PermPrincipal.php b/packages/backend-php/tests/security/PermPrincipal.php new file mode 100644 index 000000000..bc4738cc5 --- /dev/null +++ b/packages/backend-php/tests/security/PermPrincipal.php @@ -0,0 +1,86 @@ +perms = $perms; + } + + /** + * canGrantAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function canGrantAllPermissions(...$permission) { + // TODO: Implement canGrantAllPermissions() method. + return false; + } + + /** + * canGrantAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function canGrantAnyPermissions(...$permission) { + // TODO: Implement canGrantAnyPermissions() method. + return false; + } + + public function getIdentity() { + return null; + } + + /** + * hasAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function hasAllPermissions(...$permission) { + return count(array_diff($permission, $this->perms)) == 0; + } + + /** + * hasAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function hasAnyPermissions(...$permission) { + return count(array_intersect($permission, $this->perms)) > 0; + } + + public function isAnon() { + return false; + } + + public function __toString() { + return 'perm'; + } +} diff --git a/tests/services/TestService.php b/packages/backend-php/tests/services/TestService.php similarity index 90% rename from tests/services/TestService.php rename to packages/backend-php/tests/services/TestService.php index 6c70af947..db4f3415f 100644 --- a/tests/services/TestService.php +++ b/packages/backend-php/tests/services/TestService.php @@ -12,6 +12,7 @@ use tests\contracts\ITestService1; use vhs\services\Service; +/** @typescript */ class TestService extends Service implements ITestService1 { /** * @permission perm1 @@ -45,8 +46,8 @@ public function AnyPermMethod() { /** * @permission anonymous * - * @param $a - * @param $b + * @param mixed $a + * @param mixed $b * * @return mixed */ @@ -89,14 +90,19 @@ public function MultiPermMethod() { return 'MultiPermMethod!'; } - public function NoDocMethod() { + /** + * NoDocMethod. + * + * @return string + */ + public function NoDocMethod(): string { return 'NoDocMethod!'; } /** * @permission anonymous * - * @param $a + * @param mixed $a * * @return mixed */ diff --git a/tools/composer.sh b/packages/backend-php/tools/composer.sh similarity index 100% rename from tools/composer.sh rename to packages/backend-php/tools/composer.sh diff --git a/packages/backend-php/tools/generate-dto-domain-classes.php b/packages/backend-php/tools/generate-dto-domain-classes.php new file mode 100755 index 000000000..357d7b3be --- /dev/null +++ b/packages/backend-php/tools/generate-dto-domain-classes.php @@ -0,0 +1,148 @@ +#!/usr/bin/env php +type))); + + $field = [ + 'name' => $column->name, + 'type' => $baseType + ]; + + switch ($field['type']) { + case 'datetime': + case 'text': + $field['type'] = 'string'; + + break; + case 'enum': + $enumName = sprintf('%s%sEnum', $className, ucfirst($column->name)); + + $field['enum'] = [ + 'name' => $enumName, + 'values' => $column->type->values + ]; + + break; + default: + break; + } + + return $field; +} + +/** + * generateDTOFile. + * + * @param string $dtoFile + * @param string[] $content + */ +function generateDTOFile(string $dtoFile, array $content): void { + printf("Writing %s...\n", $dtoFile); + $output = []; + + $output[] = 'Table()->columns->all() as $column) { + $domains[$className][] = generateFieldDefinition($className, $column); + } + } +} + +foreach ($domains as $className => $fields) { + printf("Generating %s...\n", $className); + + $dtoFile = sprintf('app/dto/%s.php', $className); + + $classContent = []; + + $classContent[] = sprintf('class %s {', $className); + + foreach ($fields as $field) { + if ($field['type'] === 'enum') { + // @phpstan-ignore offsetAccess.notFound + $enumFile = sprintf('app/dto/%s.php', $field['enum']['name']); + + $enumContent = []; + + // @phpstan-ignore offsetAccess.notFound + $enumContent[] = sprintf('enum %s {', $field['enum']['name']); + + // @phpstan-ignore offsetAccess.notFound + foreach ($field['enum']['values'] as $value) { + $enumContent[] = sprintf(' case %s;', $value); + } + + $enumContent[] = '}'; + + generateDTOFile($enumFile, $enumContent); + + // @phpstan-ignore offsetAccess.notFound + $classContent[] = sprintf(' public %s $%s;', $field['enum']['name'], $field['name']); + } elseif ($field['type'] !== 'enum') { + $classContent[] = sprintf(' public %s $%s;', $field['type'], $field['name']); + } + + $classContent[] = ''; + } + + $classContent[] = sprintf(" public function __construct(\app\domain\%s $%s) {", $className, lcfirst($className)); + + foreach ($fields as $field) { + if ($field['type'] === 'enum') { + $classContent[] = sprintf( + ' $this->%s = %s::tryFrom($%s->%s);', + $field['name'], + // @phpstan-ignore offsetAccess.notFound + $field['enum']['name'], + lcfirst($className), + $field['name'] + ); + } else { + $classContent[] = sprintf(' $this->%s = $%s->%s;', $field['name'], lcfirst($className), $field['name']); + } + } + $classContent[] = sprintf(' }'); + + $classContent[] = '}'; + $classContent[] = ''; + + generateDTOFile($dtoFile, $classContent); +} diff --git a/packages/backend-php/tools/generate-dto-schema-classes.php b/packages/backend-php/tools/generate-dto-schema-classes.php new file mode 100644 index 000000000..16f360691 --- /dev/null +++ b/packages/backend-php/tools/generate-dto-schema-classes.php @@ -0,0 +1,72 @@ +type))); + + $field = [ + 'name' => $column->name, + 'type' => $baseType + ]; + + switch ($field['type']) { + case 'datetime': + case 'text': + $field['type'] = 'string'; + + break; + case 'enum': + $enumName = sprintf('%s%sEnum', $className, ucfirst($column->name)); + + $field['enum'] = [ + 'name' => $enumName, + /** + * @disregard P1014 + * + * @phpstan-ignore property.notFound + */ + 'values' => $column->type->values + ]; + + break; + default: + break; + } + + return $field; +} + +foreach (glob('app/schema/*.php') as $filename) { + require_once $filename; +} + +$schemas = []; + +foreach (get_declared_classes() as $class) { + if (is_subclass_of($class, '\\vhs\\domain\\Schema')) { + $schemaName = str_replace('app\\schema\\', '', $class); + $schemas[$schemaName] = $class; + + print_r($class::Table()->columns->all()); + /** @var \vhs\database\Column $column */ + foreach ($class::Table()->columns->all() as $column) { + $schemas[$schemaName][] = generateFieldDefinition($schemaName, $column); + } + } +} + +print_r($schemas); diff --git a/packages/backend-php/tools/generate-ts-contract-implementation.php b/packages/backend-php/tools/generate-ts-contract-implementation.php new file mode 100755 index 000000000..7c465f5cd --- /dev/null +++ b/packages/backend-php/tools/generate-ts-contract-implementation.php @@ -0,0 +1,57 @@ +#!/usr/bin/env php +getInterfaces() as $interface) { + if (array_key_exists('vhs\\services\\IContract', $interface->getInterfaces())) { + $contract = $interface; + } + } + + $baseInterface = PHP2TS::getBaseContractInterface($contract->getName()); + $baseService = substr($baseInterface, 1); + + printf("export default class %s implements %s {\n", $baseService, $baseInterface); + + $contractMethods = $contract->getMethods(); + + foreach ($contractMethods as $k => $contractMethod) { + if ($k > 0) { + echo "\n"; + } + + printf( + "%s\n async %s(%s): BackendResult<%s> {\n return await backendCall('/services/v2/%s.svc/%s', { %s })\n }\n", + PHP2TS::convertDocComment($contractMethod->getDocComment()), + $contractMethod->getName(), + PHP2TS::generateContractMethodArgs($contractMethod), + PHP2TS::generateContractMethodReturnType($contractMethod), + $baseService, + $contractMethod->getName(), + PHP2TS::generateContractMethodParams($contractMethod) + ); + } + + printf("}\n"); +} diff --git a/packages/backend-php/tools/generate-ts-contract-interface.php b/packages/backend-php/tools/generate-ts-contract-interface.php new file mode 100755 index 000000000..e339d74b7 --- /dev/null +++ b/packages/backend-php/tools/generate-ts-contract-interface.php @@ -0,0 +1,53 @@ +#!/usr/bin/env php +getInterfaces() as $interface) { + if (array_key_exists('vhs\\services\\IContract', $interface->getInterfaces())) { + $contract = $interface; + } + } + + $baseInterface = PHP2TS::getBaseContractInterface($contract->getName()); + + printf("export interface %s {\n", $baseInterface); + + $contractMethods = $contract->getMethods(); + + foreach ($contractMethods as $k => $contractMethod) { + if ($k > 0) { + echo "\n"; + } + + printf( + "%s\n %s: (%s) => BackendResult<%s>\n", + PHP2TS::convertDocComment($contractMethod->getDocComment()), + $contractMethod->getName(), + PHP2TS::generateContractMethodArgs($contractMethod), + PHP2TS::generateContractMethodReturnType($contractMethod) + ); + } + + printf("}\n"); +} diff --git a/tools/migrate.php b/packages/backend-php/tools/migrate.php similarity index 86% rename from tools/migrate.php rename to packages/backend-php/tools/migrate.php index f3c41aac6..89d8c11da 100644 --- a/tools/migrate.php +++ b/packages/backend-php/tools/migrate.php @@ -4,11 +4,11 @@ use vhs\migration\Backup; use vhs\migration\Migrator; -require_once dirname(__FILE__) . '/../vhs/vhs.php'; +require_once __DIR__ . '/../vhs/vhs.php'; define('_VALID_PHP', true); -require_once '../conf/config.ini.php'; +require_once __DIR__ . '/../conf/config.ini.php'; $target_version = null; $do_migrate = null; @@ -44,7 +44,7 @@ } if ($do_migrate) { $migrator = new Migrator(DB_SERVER, DB_USER, DB_PASS, DB_DATABASE, new ConsoleLogger()); - if ($migrator->migrate($target_version, '../migrations')) { + if ($migrator->migrate($target_version, __DIR__ . '/../migrations')) { print "Migration succeeded\n"; } else { print "Migration failed\n"; diff --git a/packages/backend-php/tools/sync-dto-classes.sh b/packages/backend-php/tools/sync-dto-classes.sh new file mode 100755 index 000000000..caebd92eb --- /dev/null +++ b/packages/backend-php/tools/sync-dto-classes.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +cd "$(dirname "$(realpath "$0")")/.." || exit 255 + +find app/domain/ -type f -name '*.php' -print0 | xargs -0 -I% basename % .php | sort | while read -r DOMAIN_CLASS_NAME; do + DTO_FILE="app/dto/${DOMAIN_CLASS_NAME}.php" + + cat << EOF > "${DTO_FILE}" + $array + * + * @return array + */ private function __arrayCopy(array $array) { $result = []; + foreach ($array as $key => $val) { if (is_array($val)) { $result[$key] = $this->__arrayCopy($val); @@ -32,7 +38,13 @@ private function __arrayCopy(array $array) { return $result; } - public function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { + // @phpstan-ignore foreach.nonIterable foreach ($this as $key => $val) { if (is_object($val)) { $this->{$key} = clone $val; diff --git a/packages/backend-php/vhs/Logger.php b/packages/backend-php/vhs/Logger.php new file mode 100644 index 000000000..9b5a6339e --- /dev/null +++ b/packages/backend-php/vhs/Logger.php @@ -0,0 +1,47 @@ +log(sprintf('%s:%s - %s: %s', str_replace(BasePath::getBasePath(withSlash: true), '', $file), $line, $caller, $msg)); + } + } + + /** + * __toString. + * + * @return string + */ + public function __toString() { + return get_called_class(); + } +} diff --git a/packages/backend-php/vhs/Loggington.php b/packages/backend-php/vhs/Loggington.php new file mode 100644 index 000000000..e8e59f343 --- /dev/null +++ b/packages/backend-php/vhs/Loggington.php @@ -0,0 +1,19 @@ +logger = new SilentLogger(); + } + + public function setLogger(Logger &$logger): void { + $this->logger = &$logger; + } +} diff --git a/vhs/Singleton.php b/packages/backend-php/vhs/Singleton.php similarity index 62% rename from vhs/Singleton.php rename to packages/backend-php/vhs/Singleton.php index e6fec9be8..36129d908 100644 --- a/vhs/Singleton.php +++ b/packages/backend-php/vhs/Singleton.php @@ -9,10 +9,21 @@ namespace vhs; +/** @typescript */ abstract class Singleton { + /** + * protected skeleton constructor. + * + * @return void + */ protected function __construct() { } + /** + * get instance. + * + * @return \vhs\Singleton + */ final public static function getInstance() { static $aoInstance = []; @@ -25,6 +36,11 @@ final public static function getInstance() { return $aoInstance[$class]; } - private function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { } } diff --git a/vhs/SplClassLoader.php b/packages/backend-php/vhs/SplClassLoader.php similarity index 76% rename from vhs/SplClassLoader.php rename to packages/backend-php/vhs/SplClassLoader.php index 6172a36ae..fca440034 100644 --- a/vhs/SplClassLoader.php +++ b/packages/backend-php/vhs/SplClassLoader.php @@ -2,11 +2,38 @@ namespace vhs; +/** @typescript */ class SplClassLoaderItem { + /** + * file extension. + * + * @var mixed + */ public $fileExtension; + + /** + * include path. + * + * @var mixed + */ public $includePath; + + /** + * namespace. + * + * @var mixed + */ public $namespace; + /** + * __construct. + * + * @param string|null $namespace + * @param string|null $includePath + * @param string $fileExtension + * + * @return void + */ public function __construct($namespace = null, $includePath = null, $fileExtension = '.php') { $this->namespace = $namespace; $this->includePath = $includePath; @@ -14,13 +41,30 @@ public function __construct($namespace = null, $includePath = null, $fileExtensi } } +/** @typescript */ class SplClassLoader { + /** + * namespace items. + * + * @var array + */ private $_items = []; + + /** + * namespace separator. + * + * @var string + */ private $_namespaceSeparator = '\\'; protected function __construct() { } + /** + * getInstance. + * + * @return \vhs\SplClassLoader + */ final public static function getInstance() { static $aoInstance = []; @@ -31,6 +75,13 @@ final public static function getInstance() { return $aoInstance['SplClassLoader']; } + /** + * add a new item. + * + * @param \vhs\SplClassLoaderItem $item + * + * @return void + */ public function add(SplClassLoaderItem $item) { $this->_items[$item->namespace] = $item; $this->register(); @@ -88,18 +139,26 @@ public function loadClass($className) { $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . $item->fileExtension; - //print ($item->includePath !== null ? $item->includePath . DIRECTORY_SEPARATOR : '') . $fileName; require ($item->includePath !== null ? $item->includePath . DIRECTORY_SEPARATOR : '') . $fileName; } } /** * Installs this class loader on the SPL autoload stack. + * + * @return void */ public function register() { spl_autoload_register([$this, 'loadClass']); } + /** + * remove an item. + * + * @param string $namespace + * + * @return void + */ public function remove($namespace) { if (array_key_exists($namespace, $this->_items)) { unset($this->_items[$namespace]); @@ -110,6 +169,8 @@ public function remove($namespace) { * Sets the namespace separator used by classes in the namespace of this class loader. * * @param string $sep the separator to use + * + * @return void */ public function setNamespaceSeparator($sep) { $this->_namespaceSeparator = $sep; @@ -117,11 +178,18 @@ public function setNamespaceSeparator($sep) { /** * Uninstalls this class loader from the SPL autoloader stack. + * + * @return void */ public function unregister() { spl_autoload_unregister([$this, 'loadClass']); } - private function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { } } diff --git a/vhs/database/Column.php b/packages/backend-php/vhs/database/Column.php similarity index 66% rename from vhs/database/Column.php rename to packages/backend-php/vhs/database/Column.php index 2d35aaa13..2735b671d 100644 --- a/vhs/database/Column.php +++ b/packages/backend-php/vhs/database/Column.php @@ -11,12 +11,46 @@ use vhs\database\types\Type; +/** @typescript */ class Column extends Element implements \Serializable, \JsonSerializable { + /** + * name. + * + * @var string + */ public $name; + + /** + * serializable. + * + * @var bool + */ public $serializable; + + /** + * table. + * + * @var \vhs\database\Table + */ public $table; + + /** + * type. + * + * @var \vhs\database\types\Type + */ public $type; + /** + * __construct. + * + * @param \vhs\database\Table $table + * @param string $name + * @param \vhs\database\types\Type $type + * @param ?bool $serializable + * + * @return void + */ public function __construct(Table &$table, $name, Type $type, $serializable = true) { $this->table = $table; $this->name = $name; @@ -25,7 +59,8 @@ public function __construct(Table &$table, $name, Type $type, $serializable = tr } /** - * @param IGenerator $generator + * @param \vhs\database\IGenerator $generator + * @param mixed|null $value * * @return mixed */ @@ -34,10 +69,22 @@ public function generate(IGenerator $generator, $value = null) { return $this->generateColumn($generator); } + /** + * generateColumn. + * + * @param \vhs\database\IColumnGenerator $generator + * + * @return string + */ public function generateColumn(IColumnGenerator $generator) { return $generator->generateColumn($this); } + /** + * getAbsoluteName. + * + * @return string + */ public function getAbsoluteName() { return $this->table->name . '.' . $this->name; } @@ -86,18 +133,42 @@ public function unserialize($serialized): void { // TODO: Implement unserialize() method. } - public function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { $this->type = clone $this->type; } - public function __serialize() { - return $this->serialize(); + /** + * __serialize. + * + * @return mixed[] + */ + public function __serialize(): array { + return [$this->serialize()]; } + /** + * __unserialize. + * + * @param mixed $data + * + * @return void + */ public function __unserialize($data): void { // TODO: Implement __unserialize() method. } + /** + * __updateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ public function __updateTable(Table &$table) { $this->table = $table; } diff --git a/vhs/database/Columns.php b/packages/backend-php/vhs/database/Columns.php similarity index 65% rename from vhs/database/Columns.php rename to packages/backend-php/vhs/database/Columns.php index 2b9280ddc..31d56e1b0 100644 --- a/vhs/database/Columns.php +++ b/packages/backend-php/vhs/database/Columns.php @@ -11,8 +11,10 @@ use vhs\Cloneable; +/** @typescript */ class Columns { use Cloneable; + /** @var Column[] */ public $__columns; @@ -20,14 +22,37 @@ public function __construct(Column ...$columns) { $this->__columns = $columns; } + /** + * compare Column $a to Column $b. + * + * @param Column $a + * @param Column $b + * + * @return bool + */ public static function Equals(Column $a, Column $b) { return $a->name === $b->name && $a->table === $b->table && $a->type === $b->type; } + /** + * Check if Column $a's name matches $name. + * + * @param Column $a + * @param string $name + * + * @return bool + */ public static function EqualsByName(Column $a, $name) { return $a->name === $name; } + /** + * add a column definition. + * + * @param Column $column + * + * @return \vhs\database\Column|null + */ public function add(Column $column) { if (!$this->contains($column->name)) { array_push($this->__columns, $column); @@ -45,6 +70,13 @@ public function all() { return $this->__columns; } + /** + * check if Column set contains the specified column name. + * + * @param string $column + * + * @return bool + */ public function contains($column) { foreach ($this->__columns as $col) { if ($col->name == $column) { @@ -55,6 +87,13 @@ public function contains($column) { return false; } + /** + * get a column by name. + * + * @param string $name + * + * @return \vhs\database\Column|null + */ public function getByName($name) { foreach ($this->__columns as $col) { if ($col->name == $name) { @@ -65,6 +104,11 @@ public function getByName($name) { return null; } + /** + * get column names. + * + * @return string[] + */ public function names() { $names = []; @@ -75,6 +119,13 @@ public function names() { return $names; } + /** + * remove a column. + * + * @param Column $column + * + * @return void + */ public function remove(Column $column) { if (!$this->contains($column->name)) { return; @@ -87,6 +138,13 @@ public function remove(Column $column) { } } + /** + * column getter. + * + * @param string $name + * + * @return \vhs\database\Column|null + */ public function __get($name) { return $this->getByName($name); } diff --git a/vhs/database/ConnectionInfo.php b/packages/backend-php/vhs/database/ConnectionInfo.php similarity index 64% rename from vhs/database/ConnectionInfo.php rename to packages/backend-php/vhs/database/ConnectionInfo.php index 2546dacb5..637f2d3bd 100644 --- a/vhs/database/ConnectionInfo.php +++ b/packages/backend-php/vhs/database/ConnectionInfo.php @@ -9,9 +9,20 @@ namespace vhs\database; +/** @typescript */ abstract class ConnectionInfo { + /** + * getDetails. + * + * @return array + */ abstract public function getDetails(); + /** + * __toString. + * + * @return string + */ public function __toString() { return var_export($this->getDetails(), true); } diff --git a/vhs/database/Database.php b/packages/backend-php/vhs/database/Database.php similarity index 68% rename from vhs/database/Database.php rename to packages/backend-php/vhs/database/Database.php index 9db0e0ce2..1c0506bff 100644 --- a/vhs/database/Database.php +++ b/packages/backend-php/vhs/database/Database.php @@ -12,7 +12,6 @@ use vhs\database\engines\memory\InMemoryEngine; use vhs\database\exceptions\DatabaseConnectionException; use vhs\database\exceptions\DatabaseException; -use vhs\database\queries\Query; use vhs\database\queries\QueryCount; use vhs\database\queries\QueryDelete; use vhs\database\queries\QueryInsert; @@ -22,85 +21,170 @@ use vhs\loggers\SilentLogger; use vhs\Singleton; +/** + * @method static \vhs\database\Database getInstance() + * + * @typescript + */ class Database extends Singleton { /** @var Engine */ private $engine; + /** @var Logger */ private $logger; + /** @var bool */ private $rethrow; + /** + * __construct. + * + * @return void + */ protected function __construct() { $this->setLoggerInternal(new SilentLogger()); $this->setEngineInternal(new InMemoryEngine()); $this->setRethrowInternal(true); } + /** + * __destruct. + * + * @return void + */ public function __destruct() { $this->engine->disconnect(); } + /** + * arbitrary. + * + * @param mixed $command + * + * @return mixed + */ public static function arbitrary($command) { /** @var Database $db */ $db = self::getInstance(); + return $db->invokeEngine(function () use ($db, $command) { - //TODO warn that this is not ideal + // TODO warn that this is not ideal return $db->engine->arbitrary($command); }); } + /** + * count. + * + * @param \vhs\database\queries\QueryCount $query + * + * @return mixed + */ public static function count(QueryCount $query) { /** @var Database $db */ $db = self::getInstance(); + return $db->invokeEngine(function () use ($db, $query) { return $db->engine->count($query); }); } + /** + * DateFormat. + * + * @return string + */ public static function DateFormat() { return self::getInstance()->engine->DateFormat(); } + /** + * delete. + * + * @param \vhs\database\queries\QueryDelete $query + * + * @return mixed + */ public static function delete(QueryDelete $query) { /** @var Database $db */ $db = self::getInstance(); + return $db->invokeEngine(function () use ($db, $query) { return $db->engine->delete($query); }); } + /** + * exists. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @return mixed + */ public static function exists(QuerySelect $query) { /** @var Database $db */ $db = self::getInstance(); + return $db->invokeEngine(function () use ($db, $query) { return $db->engine->exists($query); }); } + /** + * insert. + * + * @param \vhs\database\queries\QueryInsert $query + * + * @return mixed + */ public static function insert(QueryInsert $query) { /** @var Database $db */ $db = self::getInstance(); + return $db->invokeEngine(function () use ($db, $query) { return $db->engine->insert($query); }); } + /** + * scalar. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @return mixed + */ public static function scalar(QuerySelect $query) { /** @var Database $db */ $db = self::getInstance(); + return $db->invokeEngine(function () use ($db, $query) { return $db->engine->scalar($query); }); } + /** + * select. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @return mixed + */ public static function select(QuerySelect $query) { /** @var Database $db */ $db = self::getInstance(); + return $db->invokeEngine(function () use ($db, $query) { return $db->engine->select($query); }); } + /** + * setEngine. + * + * @param \vhs\database\Engine $engine + * + * @return void + */ public static function setEngine(Engine $engine) { /** @var Database $db */ $db = self::getInstance(); @@ -108,12 +192,26 @@ public static function setEngine(Engine $engine) { $db->setEngineInternal($engine); } + /** + * setLogger. + * + * @param \vhs\Logger $logger + * + * @return void + */ public static function setLogger(Logger $logger) { $db = self::getInstance(); $db->setLoggerInternal($logger); } + /** + * setRethrow. + * + * @param mixed $rethrow + * + * @return void + */ public static function setRethrow($rethrow) { /** @var Database $db */ $db = self::getInstance(); @@ -121,14 +219,29 @@ public static function setRethrow($rethrow) { $db->setRethrowInternal($rethrow); } + /** + * update. + * + * @param \vhs\database\queries\QueryUpdate $query + * + * @return mixed + */ public static function update(QueryUpdate $query) { /** @var Database $db */ $db = self::getInstance(); + return $db->invokeEngine(function () use ($db, $query) { return $db->engine->update($query); }); } + /** + * handleException. + * + * @param mixed $exception + * + * @return void + */ private function handleException($exception) { $this->logger->log($exception); @@ -137,6 +250,13 @@ private function handleException($exception) { } } + /** + * invokeEngine. + * + * @param callable $func + * + * @return mixed + */ private function invokeEngine(callable $func) { try { $this->engine->connect(); @@ -155,6 +275,13 @@ private function invokeEngine(callable $func) { return $retval; } + /** + * setEngineInternal. + * + * @param \vhs\database\Engine $engine + * + * @return void + */ private function setEngineInternal(Engine $engine) { if (!is_null($this->engine)) { $this->engine->disconnect(); @@ -163,10 +290,24 @@ private function setEngineInternal(Engine $engine) { $this->engine = $engine; } + /** + * setLoggerInternal. + * + * @param \vhs\Logger $logger + * + * @return void + */ private function setLoggerInternal(Logger $logger) { $this->logger = $logger; } + /** + * setRethrowInternal. + * + * @param mixed $rethrow + * + * @return void + */ private function setRethrowInternal($rethrow) { $this->rethrow = $rethrow; } diff --git a/vhs/database/Element.php b/packages/backend-php/vhs/database/Element.php similarity index 66% rename from vhs/database/Element.php rename to packages/backend-php/vhs/database/Element.php index 92b4e35d9..0594fdef1 100644 --- a/vhs/database/Element.php +++ b/packages/backend-php/vhs/database/Element.php @@ -11,9 +11,17 @@ use vhs\Cloneable; +/** @typescript */ abstract class Element implements IGeneratable { use Cloneable; + /** + * __updateTable. + * + * @param \vhs\database\Table &$table + * + * @return mixed + */ protected function __updateTable(Table &$table) { } } diff --git a/vhs/database/Engine.php b/packages/backend-php/vhs/database/Engine.php similarity index 57% rename from vhs/database/Engine.php rename to packages/backend-php/vhs/database/Engine.php index 6d34819e4..fe0f4f0eb 100644 --- a/vhs/database/Engine.php +++ b/packages/backend-php/vhs/database/Engine.php @@ -11,13 +11,36 @@ use vhs\Logger; +/** @typescript */ abstract class Engine implements IDataInterface { + /** + * connect. + * + * @return bool + */ abstract public function connect(); + /** + * disconnect. + * + * @return mixed + */ abstract public function disconnect(); + /** + * setLogger. + * + * @param \vhs\Logger $logger + * + * @return void + */ abstract public function setLogger(Logger $logger); + /** + * __toString. + * + * @return string + */ public function __toString() { return get_called_class(); } diff --git a/vhs/database/IColumnGenerator.php b/packages/backend-php/vhs/database/IColumnGenerator.php similarity index 61% rename from vhs/database/IColumnGenerator.php rename to packages/backend-php/vhs/database/IColumnGenerator.php index 1b21d0faa..6f697511b 100644 --- a/vhs/database/IColumnGenerator.php +++ b/packages/backend-php/vhs/database/IColumnGenerator.php @@ -9,6 +9,14 @@ namespace vhs\database; +/** @typescript */ interface IColumnGenerator extends IGenerator { + /** + * generateColumn. + * + * @param \vhs\database\Column $column + * + * @return mixed + */ public function generateColumn(Column $column); } diff --git a/vhs/database/IConverter.php b/packages/backend-php/vhs/database/IConverter.php similarity index 88% rename from vhs/database/IConverter.php rename to packages/backend-php/vhs/database/IConverter.php index 895a77114..36db7a663 100644 --- a/vhs/database/IConverter.php +++ b/packages/backend-php/vhs/database/IConverter.php @@ -9,5 +9,6 @@ namespace vhs\database; +/** @typescript */ interface IConverter { } diff --git a/vhs/database/IConvertible.php b/packages/backend-php/vhs/database/IConvertible.php similarity index 94% rename from vhs/database/IConvertible.php rename to packages/backend-php/vhs/database/IConvertible.php index cd51b4f5d..5874e2083 100644 --- a/vhs/database/IConvertible.php +++ b/packages/backend-php/vhs/database/IConvertible.php @@ -9,6 +9,7 @@ namespace vhs\database; +/** @typescript */ interface IConvertible { /** * @param IConverter $converter diff --git a/packages/backend-php/vhs/database/IDataInterface.php b/packages/backend-php/vhs/database/IDataInterface.php new file mode 100644 index 000000000..90351a655 --- /dev/null +++ b/packages/backend-php/vhs/database/IDataInterface.php @@ -0,0 +1,114 @@ +where = $where; } + /** + * Where. + * + * @param \vhs\database\wheres\Where $where + * + * @return \vhs\database\On + */ public static function Where(Where $where) { return new On($where); } /** - * @param IGenerator $generator - * @param null $value + * @param \vhs\database\IOnGenerator $generator + * @param mixed $value * * @return mixed */ public function generate(IGenerator $generator, $value = null) { - /** @var IOnGenerator $generator */ return $this->generateOn($generator); } + /** + * generateOn. + * + * @param \vhs\database\IOnGenerator $generator + * + * @return mixed + */ public function generateOn(IOnGenerator $generator) { return $generator->generateOn($this); } + /** + * __updateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ public function __updateTable(Table &$table) { $this->where->__updateTable($table); } diff --git a/vhs/database/Table.php b/packages/backend-php/vhs/database/Table.php similarity index 69% rename from vhs/database/Table.php rename to packages/backend-php/vhs/database/Table.php index 18862bbf4..6475733cb 100644 --- a/vhs/database/Table.php +++ b/packages/backend-php/vhs/database/Table.php @@ -15,30 +15,44 @@ use vhs\database\joins\Join; use vhs\database\types\Type; +/** @typescript */ class Table extends Element { - /** @var int */ + /** + * cloneIndex. + * + * @var int + */ private static $cloneIndex = 0; - /** @var null|string */ + + /** + * alias. + * + * @var string|null + */ public $alias; /** @var string */ public $aliasPrefix; - /** @var IAccess[] */ + + /** @var \vhs\database\access\IAccess[] */ public $checks; - /** @var Columns */ + + /** @var \vhs\database\Columns */ public $columns; - /** @var Constraint[] */ + + /** @var \vhs\database\constraints\Constraint[] */ public $constraints; - /** @var Join[] */ + + /** @var \vhs\database\joins\Join[] */ public $joins; /** @var string */ public $name; /** - * @param string $name - * @param string $alias - * @param Join|joins\Join[] $join + * @param string $name + * @param string|null $alias + * @param \vhs\database\joins\Join ...$join */ public function __construct($name, $alias = null, Join ...$join) { $this->name = $name; @@ -62,12 +76,22 @@ public function __construct($name, $alias = null, Join ...$join) { $this->columns = new Columns(); } + /** + * add a column. + * + * @param string $name + * @param \vhs\database\types\Type $type + * @param bool $serializable + * + * @return \vhs\database\Column|null + */ public function addColumn($name, Type $type, $serializable = true) { return $this->columns->add(new Column($this, $name, $type, $serializable)); } /** - * @param IGenerator $generator + * @param \vhs\database\IGenerator $generator + * @param mixed|null $value * * @return mixed */ @@ -99,15 +123,34 @@ public function getPrimaryKeys() { return $pks; } + /** + * Undocumented function. + * + * @param \vhs\database\access\IAccess ...$checks + * + * @return void + */ public function setAccess(IAccess ...$checks) { $this->checks = $checks; } + /** + * set constraints. + * + * @param \vhs\database\constraints\Constraint ...$constraints + * + * @return void + */ public function setConstraints(Constraint ...$constraints) { $this->constraints = $constraints; } - public function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { parent::__clone(); self::$cloneIndex += 1; diff --git a/packages/backend-php/vhs/database/access/IAccess.php b/packages/backend-php/vhs/database/access/IAccess.php new file mode 100644 index 000000000..87af9c158 --- /dev/null +++ b/packages/backend-php/vhs/database/access/IAccess.php @@ -0,0 +1,38 @@ +column = $column; } + /** + * ForeignKey. + * + * @param \vhs\database\Column $column + * @param \vhs\database\Table $foreignTable + * @param \vhs\database\Column $on + * + * @return \vhs\database\constraints\ForeignKey + */ public static function ForeignKey(Column $column, Table &$foreignTable, Column $on) { return new ForeignKey($column, $foreignTable, $on); } + /** + * PrimaryKey. + * + * @param \vhs\database\Column $column + * + * @return \vhs\database\constraints\PrimaryKey + */ public static function PrimaryKey(Column $column) { return new PrimaryKey($column); } + /** + * generateConstraint. + * + * @param \vhs\database\constraints\IConstraintGenerator $generator + * + * @return mixed + */ abstract public function generateConstraint(IConstraintGenerator $generator); /** - * @param IGenerator $generator + * @param \vhs\database\IGenerator $generator + * @param mixed|null $value * * @return mixed */ @@ -41,6 +78,13 @@ public function generate(IGenerator $generator, $value = null) { return $this->generateConstraint($generator); } + /** + * __updateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ public function __updateTable(Table &$table) { $this->column->__updateTable($table); } diff --git a/vhs/database/constraints/ForeignKey.php b/packages/backend-php/vhs/database/constraints/ForeignKey.php similarity index 87% rename from vhs/database/constraints/ForeignKey.php rename to packages/backend-php/vhs/database/constraints/ForeignKey.php index 90f97c0a3..f55fd99ff 100644 --- a/vhs/database/constraints/ForeignKey.php +++ b/packages/backend-php/vhs/database/constraints/ForeignKey.php @@ -12,6 +12,7 @@ use vhs\database\Column; use vhs\database\Table; +/** @typescript */ class ForeignKey extends Constraint { /** @var Column */ public $on; @@ -29,7 +30,12 @@ public function generateConstraint(IConstraintGenerator $generator) { return $generator->generateForeignKey($this); } - public function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { $this->on = clone $this->on; } diff --git a/vhs/database/constraints/IConstraintGenerator.php b/packages/backend-php/vhs/database/constraints/IConstraintGenerator.php similarity index 53% rename from vhs/database/constraints/IConstraintGenerator.php rename to packages/backend-php/vhs/database/constraints/IConstraintGenerator.php index 6234fca44..12c8e4bd9 100644 --- a/vhs/database/constraints/IConstraintGenerator.php +++ b/packages/backend-php/vhs/database/constraints/IConstraintGenerator.php @@ -11,8 +11,23 @@ use vhs\database\IGenerator; +/** @typescript */ interface IConstraintGenerator extends IGenerator { + /** + * generateForeignKey. + * + * @param \vhs\database\constraints\ForeignKey $constraint + * + * @return mixed + */ public function generateForeignKey(ForeignKey $constraint); + /** + * generatePrimaryKey. + * + * @param \vhs\database\constraints\PrimaryKey $constraint + * + * @return mixed + */ public function generatePrimaryKey(PrimaryKey $constraint); } diff --git a/vhs/database/constraints/PrimaryKey.php b/packages/backend-php/vhs/database/constraints/PrimaryKey.php similarity index 94% rename from vhs/database/constraints/PrimaryKey.php rename to packages/backend-php/vhs/database/constraints/PrimaryKey.php index 94fa0f39a..e179289db 100644 --- a/vhs/database/constraints/PrimaryKey.php +++ b/packages/backend-php/vhs/database/constraints/PrimaryKey.php @@ -9,6 +9,7 @@ namespace vhs\database\constraints; +/** @typescript */ class PrimaryKey extends Constraint { public function generateConstraint(IConstraintGenerator $generator) { return $generator->generatePrimaryKey($this); diff --git a/vhs/database/engines/memory/InMemoryEngine.php b/packages/backend-php/vhs/database/engines/memory/InMemoryEngine.php similarity index 73% rename from vhs/database/engines/memory/InMemoryEngine.php rename to packages/backend-php/vhs/database/engines/memory/InMemoryEngine.php index dc092c642..d0ecae672 100644 --- a/vhs/database/engines/memory/InMemoryEngine.php +++ b/packages/backend-php/vhs/database/engines/memory/InMemoryEngine.php @@ -9,52 +9,105 @@ namespace vhs\database\engines\memory; -use vhs\database\Column; -use vhs\database\Columns; use vhs\database\Engine; -use vhs\database\orders\OrderBy; -use vhs\database\queries\Query; use vhs\database\queries\QueryCount; use vhs\database\queries\QueryDelete; use vhs\database\queries\QueryInsert; use vhs\database\queries\QuerySelect; use vhs\database\queries\QueryUpdate; -use vhs\database\Table; -use vhs\database\wheres\Where; use vhs\Logger; use vhs\loggers\SilentLogger; +/** @typescript */ class InMemoryEngine extends Engine { + /** + * datastore. + * + * @var array + */ private $datastore = []; + + /** + * generator. + * + * @var mixed + */ private $generator; + + /** + * keyIncrementors. + * + * @var array + */ private $keyIncrementors = []; + + /** + * logger. + * + * @var \vhs\Logger + */ private $logger; + /** + * __construct. + * + * @return void + */ public function __construct() { $this->logger = new SilentLogger(); $this->generator = new InMemoryGenerator(); } + /** + * connect. + * + * @return bool + */ public function connect() { return true; //if we can't "connect" to memory but got this far, I don't know whatever } + /** + * disconnect. + * + * @return bool + */ public function disconnect() { $this->keyIncrementors = []; $this->datastore = []; $this->logger->log('disconnected'); + return true; // ha } + /** + * DateFormat. + * + * @return string + */ public static function DateFormat() { return 'Y-m-d H:i:s'; } + /** + * arbitrary. + * + * @param mixed $command + * + * @return bool + */ public function arbitrary($command) { return false; } + /** + * count. + * + * @param \vhs\database\queries\QueryCount $query + * + * @return bool|int + */ public function count(QueryCount $query) { $this->logger->log('count ' . $query->table->name . ' ' . $query->where); if (!array_key_exists($query->table->name, $this->datastore)) { @@ -67,9 +120,10 @@ public function count(QueryCount $query) { return true; }; - if (isset($orderBy) || isset($limit)) { - throw new \Exception('TODO implement OrderBy and limit for InMemoryEngine'); - } + // TODO currently not in use, always false: remove or implement + // if (isset($orderBy) || isset($limit)) { + // throw new \Exception('TODO implement OrderBy and limit for InMemoryEngine'); + // } $count = 0; @@ -82,6 +136,13 @@ public function count(QueryCount $query) { return $count; } + /** + * delete. + * + * @param \vhs\database\queries\QueryDelete $query + * + * @return bool + */ public function delete(QueryDelete $query) { $this->logger->log('delete ' . $query->table->name . ' ' . $query->where); if (!array_key_exists($query->table->name, $this->datastore)) { @@ -103,11 +164,26 @@ public function delete(QueryDelete $query) { return true; } + /** + * exists. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @return bool + */ public function exists(QuerySelect $query) { $this->logger->log('exists: '); + return $this->count(new QueryCount($query->table, $query->where, $query->orderBy, $query->limit, $query->offset)) > 0; } + /** + * insert. + * + * @param \vhs\database\queries\QueryInsert $query + * + * @return mixed + */ public function insert(QueryInsert $query) { $this->logger->log('insert ' . $query->table->name . ' ' . var_export($query->values, true)); if (!array_key_exists($query->table->name, $this->datastore)) { @@ -136,6 +212,13 @@ public function insert(QueryInsert $query) { return $pks; } + /** + * scalar. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @return mixed + */ public function scalar(QuerySelect $query) { $this->logger->log('scalar: '); @@ -148,6 +231,13 @@ public function scalar(QuerySelect $query) { } } + /** + * select. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @return mixed + */ public function select(QuerySelect $query) { $this->logger->log('select ' . $query->table->name . ' ' . implode(', ', $query->columns->names()) . ' ' . $query->where); @@ -161,9 +251,10 @@ public function select(QuerySelect $query) { return true; }; - if (isset($orderBy) || isset($limit)) { - throw new \Exception('TODO implement OrderBy and limit for InMemoryEngine'); - } + // TODO currently not in use, always false: remove or implement + // if (isset($orderBy) || isset($limit)) { + // throw new \Exception('TODO implement OrderBy and limit for InMemoryEngine'); + // } $results = []; @@ -184,10 +275,24 @@ public function select(QuerySelect $query) { return $results; } + /** + * setLogger. + * + * @param \vhs\Logger $logger + * + * @return void + */ public function setLogger(Logger $logger) { $this->logger = $logger; } + /** + * update. + * + * @param \vhs\database\queries\QueryUpdate $query + * + * @return bool + */ public function update(QueryUpdate $query) { $this->logger->log('update ' . $query->table->name . ' ' . var_export($query->values, true) . ' ' . $query->where); if (!array_key_exists($query->table->name, $this->datastore)) { diff --git a/vhs/database/engines/memory/InMemoryGenerator.php b/packages/backend-php/vhs/database/engines/memory/InMemoryGenerator.php similarity index 62% rename from vhs/database/engines/memory/InMemoryGenerator.php rename to packages/backend-php/vhs/database/engines/memory/InMemoryGenerator.php index 7193bd1ae..9c19242f7 100644 --- a/vhs/database/engines/memory/InMemoryGenerator.php +++ b/packages/backend-php/vhs/database/engines/memory/InMemoryGenerator.php @@ -52,6 +52,7 @@ use vhs\database\wheres\WhereComparator; use vhs\database\wheres\WhereOr; +/** @typescript */ class InMemoryGenerator implements IWhereGenerator, IOrderByGenerator, @@ -64,6 +65,13 @@ class InMemoryGenerator implements ILimitGenerator, IOffsetGenerator, IOnGenerator { + /** + * generateAnd. + * + * @param \vhs\database\wheres\WhereAnd $where + * + * @return callable + */ public function generateAnd(WhereAnd $where) { $wheres = []; @@ -72,8 +80,14 @@ public function generateAnd(WhereAnd $where) { array_push($wheres, $w->generate($this)); } + /** + * lambda. + * + * @return bool + */ return function ($row) use ($wheres) { $b = true; + foreach ($wheres as $w) { $b = $b && $w($row); } @@ -82,19 +96,48 @@ public function generateAnd(WhereAnd $where) { }; } + /** + * generateAscending. + * + * @param \vhs\database\orders\OrderByAscending $ascending + * + * @return void + */ public function generateAscending(OrderByAscending $ascending) { // TODO: Implement generateAscending() method. throw new \Exception('TODO: Implement generateAscending() method.'); } - public function generateBool(TypeBool $type, $value = null) { + /** + * generateBool. + * + * @param \vhs\database\types\TypeBool $type + * @param mixed $value + * + * @return void + */ + public function generateBool(TypeBool $type, mixed $value = null) { // TODO: Implement generateBool() method. } + /** + * generateColumn. + * + * @param \vhs\database\Column $column + * + * @return void + */ public function generateColumn(Column $column) { // TODO: Implement generateColumn() method. } + /** + * generateComparator. + * + * @param \vhs\database\wheres\WhereComparator $where + * + * @return callable + */ public function generateComparator(WhereComparator $where) { return function ($row) use ($where) { $column = $where->column->name; @@ -180,67 +223,184 @@ public function generateComparator(WhereComparator $where) { }; } + /** + * generateCross. + * + * @param \vhs\database\joins\JoinCross $join + * + * @return void + */ public function generateCross(JoinCross $join) { // TODO: Implement generateCross() method. } - public function generateDate(TypeDate $type, $value = null) { + /** + * generateDate. + * + * @param \vhs\database\types\TypeDate $type + * @param mixed $value + * + * @return void + */ + public function generateDate(TypeDate $type, mixed $value = null) { // TODO: Implement generateDate() method. } - public function generateDateTime(TypeDateTime $type, $value = null) { + /** + * generateDateTime. + * + * @param \vhs\database\types\TypeDateTime $type + * @param mixed $value + * + * @return void + */ + public function generateDateTime(TypeDateTime $type, mixed $value = null) { // TODO: Implement generateDateTime() method. } + /** + * generateDelete. + * + * @param \vhs\database\queries\QueryDelete $query + * + * @return void + */ public function generateDelete(QueryDelete $query) { // TODO: Implement generateDelete() method. } + /** + * generateDescending. + * + * @param \vhs\database\orders\OrderByDescending $descending + * + * @return void + */ public function generateDescending(OrderByDescending $descending) { // TODO: Implement generateDescending() method. throw new \Exception('Implement generateDescending() method.'); } - public function generateEnum(TypeEnum $type, $value = null) { + /** + * generateEnum. + * + * @param \vhs\database\types\TypeEnum $type + * @param mixed $value + * + * @return void + */ + public function generateEnum(TypeEnum $type, mixed $value = null) { // TODO: Implement generateEnum() method. } - public function generateFloat(TypeFloat $type, $value = null) { + /** + * generateFloat. + * + * @param \vhs\database\types\TypeFloat $type + * @param mixed $value + * + * @return void + */ + public function generateFloat(TypeFloat $type, mixed $value = null) { // TODO: Implement generateFloat() method. } + /** + * generateForeignKey. + * + * @param \vhs\database\constraints\ForeignKey $constraint + * + * @return void + */ public function generateForeignKey(ForeignKey $constraint) { // TODO: Implement generateForeignKey() method. } + /** + * generateInner. + * + * @param \vhs\database\joins\JoinInner $join + * + * @return void + */ public function generateInner(JoinInner $join) { // TODO: Implement generateInner() method. } + /** + * generateInsert. + * + * @param \vhs\database\queries\QueryInsert $query + * + * @return void + */ public function generateInsert(QueryInsert $query) { // TODO: Implement generateInsert() method. } - public function generateInt(TypeInt $type, $value = null) { + /** + * generateInt. + * + * @param \vhs\database\types\TypeInt $type + * @param mixed $value + * + * @return void + */ + public function generateInt(TypeInt $type, mixed $value = null) { // TODO: Implement generateInt() method. } + /** + * generateLeft. + * + * @param \vhs\database\joins\JoinLeft $join + * + * @return void + */ public function generateLeft(JoinLeft $join) { // TODO: Implement generateLeft() method. } + /** + * generateLimit. + * + * @param \vhs\database\limits\Limit $limit + * + * @return int + */ public function generateLimit(Limit $limit) { return $limit->limit; } + /** + * generateOffset. + * + * @param Offset $offset + * + * @return mixed + */ public function generateOffset(Offset $offset) { return $offset->offset; } + /** + * generateOn. + * + * @param On $on + * + * @return void + */ public function generateOn(On $on) { // TODO: Implement generateOn() method. } + /** + * generateOr. + * + * @param WhereOr $where + * + * @return callable + */ public function generateOr(WhereOr $where) { $wheres = []; @@ -259,38 +419,103 @@ public function generateOr(WhereOr $where) { }; } + /** + * generateOuter. + * + * @param JoinOuter $join + * + * @return void + */ public function generateOuter(JoinOuter $join) { // TODO: Implement generateOuter() method. } + /** + * generatePrimaryKey. + * + * @param PrimaryKey $constraint + * + * @return void + */ public function generatePrimaryKey(PrimaryKey $constraint) { // TODO: Implement generatePrimaryKey() method. } + /** + * generateRight. + * + * @param JoinRight $join + * + * @return void + */ public function generateRight(JoinRight $join) { // TODO: Implement generateRight() method. } + /** + * generateSelect. + * + * @param QuerySelect $query + * + * @return void + */ public function generateSelect(QuerySelect $query) { // TODO: Implement generateSelect() method. } + /** + * generateSelectCount. + * + * @param QueryCount $query + * + * @return void + */ public function generateSelectCount(QueryCount $query) { // TODO: Implement generateSelect() method. } + /** + * generateString. + * + * @param TypeString $type + * @param mixed $value + * + * @return void + */ public function generateString(TypeString $type, $value = null) { // TODO: Implement generateString() method. } - public function generateTable(Table $ascending) { + /** + * generateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ + public function generateTable(Table $table) { // TODO: Implement generateTable() method. } - public function generateText(TypeText $type, $value = null) { + /** + * generateText. + * + * @param \vhs\database\types\TypeText $type + * @param mixed $value + * + * @return void + */ + public function generateText(TypeText $type, mixed $value = null) { // TODO: Implement generateText() method. } + /** + * generateUpdate. + * + * @param \vhs\database\queries\QueryUpdate $query + * + * @return void + */ public function generateUpdate(QueryUpdate $query) { // TODO: Implement generateUpdate() method. } diff --git a/vhs/database/engines/mysql/MySqlConnectionInfo.php b/packages/backend-php/vhs/database/engines/mysql/MySqlConnectionInfo.php similarity index 54% rename from vhs/database/engines/mysql/MySqlConnectionInfo.php rename to packages/backend-php/vhs/database/engines/mysql/MySqlConnectionInfo.php index 2e5c7016f..9cf93c036 100644 --- a/vhs/database/engines/mysql/MySqlConnectionInfo.php +++ b/packages/backend-php/vhs/database/engines/mysql/MySqlConnectionInfo.php @@ -11,25 +11,69 @@ use vhs\database\ConnectionInfo; +/** @typescript */ class MySqlConnectionInfo extends ConnectionInfo { + /** + * database. + * + * @var string + */ private $database; + + /** + * password. + * + * @var string + */ private $password; + + /** + * server. + * + * @var string + */ private $server; + + /** + * username. + * + * @var string + */ private $username; + /** + * __construct. + * + * @param string $server + * @param string $username + * @param string $password + * @param string $database + * + * @return void + */ public function __construct($server, $username, $password, $database = null) { $this->server = $server; $this->username = $username; $this->password = $password; $this->database = $database; - //TODO throw argument exceptions here if shit is rotten + // TODO throw argument exceptions here if shit is rotten } + /** + * getDatabase. + * + * @return string + */ public function getDatabase() { return $this->database; } + /** + * getDetails. + * + * @return array{server:string,username:string,password:string,database:string} + */ public function getDetails() { return [ 'server' => $this->server, @@ -39,14 +83,29 @@ public function getDetails() { ]; } + /** + * getPassword. + * + * @return string + */ public function getPassword() { return $this->password; } + /** + * getServer. + * + * @return string + */ public function getServer() { return $this->server; } + /** + * getUsername. + * + * @return string + */ public function getUsername() { return $this->username; } diff --git a/vhs/database/engines/mysql/MySqlConverter.php b/packages/backend-php/vhs/database/engines/mysql/MySqlConverter.php similarity index 66% rename from vhs/database/engines/mysql/MySqlConverter.php rename to packages/backend-php/vhs/database/engines/mysql/MySqlConverter.php index 65076708e..c90d6950d 100644 --- a/vhs/database/engines/mysql/MySqlConverter.php +++ b/packages/backend-php/vhs/database/engines/mysql/MySqlConverter.php @@ -19,7 +19,16 @@ use vhs\database\types\TypeString; use vhs\database\types\TypeText; +/** @typescript */ class MySqlConverter implements ITypeConverter { + /** + * convertBool. + * + * @param \vhs\database\types\TypeBool $type + * @param bool $value + * + * @return bool|null + */ public function convertBool(TypeBool $type, $value = null) { if (is_null($value)) { if (!$type->nullable) { @@ -32,6 +41,14 @@ public function convertBool(TypeBool $type, $value = null) { return boolval($value); } + /** + * convertDate. + * + * @param \vhs\database\types\TypeDate $type + * @param string|null $value + * + * @return string|null + */ public function convertDate(TypeDate $type, $value = null) { if (is_null($value)) { if (!$type->nullable) { @@ -44,8 +61,16 @@ public function convertDate(TypeDate $type, $value = null) { return date('Y-m-d', strtotime(str_replace('-', '/', $value))); } + /** + * convertDateTime. + * + * @param \vhs\database\types\TypeDateTime $type + * @param string|null $value + * + * @return string|null + */ public function convertDateTime(TypeDateTime $type, $value = null) { - if (is_null($value)) { + if (is_null($value) || empty($value)) { if (!$type->nullable) { return $type->default; } else { @@ -56,6 +81,14 @@ public function convertDateTime(TypeDateTime $type, $value = null) { return date('Y-m-d H:i:s', strtotime(str_replace('-', '/', $value))); } + /** + * convertEnum. + * + * @param \vhs\database\types\TypeEnum $type + * @param string|null $value + * + * @return string|null + */ public function convertEnum(TypeEnum $type, $value = null) { if (is_null($value)) { if (!$type->nullable) { @@ -72,6 +105,14 @@ public function convertEnum(TypeEnum $type, $value = null) { } } + /** + * convertFloat. + * + * @param \vhs\database\types\TypeFloat $type + * @param float|null $value + * + * @return float|null + */ public function convertFloat(TypeFloat $type, $value = null) { if (is_null($value)) { if (!$type->nullable) { @@ -84,6 +125,14 @@ public function convertFloat(TypeFloat $type, $value = null) { return floatval($value); } + /** + * convertInt. + * + * @param \vhs\database\types\TypeInt $type + * @param int|null $value + * + * @return int|null + */ public function convertInt(TypeInt $type, $value = null) { if (is_null($value)) { if (!$type->nullable) { @@ -96,6 +145,14 @@ public function convertInt(TypeInt $type, $value = null) { return intval($value); } + /** + * convertString. + * + * @param \vhs\database\types\TypeString $type + * @param string|null $value + * + * @return string|null + */ public function convertString(TypeString $type, $value = null) { if (is_null($value)) { if (!$type->nullable) { @@ -108,6 +165,14 @@ public function convertString(TypeString $type, $value = null) { return (string) $value; } + /** + * convertText. + * + * @param \vhs\database\types\TypeText $type + * @param string|null $value + * + * @return string|null + */ public function convertText(TypeText $type, $value = null) { if (is_null($value)) { if (!$type->nullable) { diff --git a/vhs/database/engines/mysql/MySqlEngine.php b/packages/backend-php/vhs/database/engines/mysql/MySqlEngine.php similarity index 68% rename from vhs/database/engines/mysql/MySqlEngine.php rename to packages/backend-php/vhs/database/engines/mysql/MySqlEngine.php index e90945cf2..b4e6b16bd 100644 --- a/vhs/database/engines/mysql/MySqlEngine.php +++ b/packages/backend-php/vhs/database/engines/mysql/MySqlEngine.php @@ -10,37 +10,69 @@ namespace vhs\database\engines\mysql; use vhs\database\Column; -use vhs\database\Columns; use vhs\database\Engine; use vhs\database\exceptions\DatabaseConnectionException; use vhs\database\exceptions\DatabaseException; -use vhs\database\limits\Limit; -use vhs\database\offsets\Offset; -use vhs\database\orders\OrderBy; -use vhs\database\queries\Query; use vhs\database\queries\QueryCount; use vhs\database\queries\QueryDelete; use vhs\database\queries\QueryInsert; use vhs\database\queries\QuerySelect; use vhs\database\queries\QueryUpdate; -use vhs\database\Table; -use vhs\database\types\Type; -use vhs\database\wheres\Where; use vhs\Logger; use vhs\loggers\SilentLogger; +/** @typescript */ class MySqlEngine extends Engine { + /** + * autoCreateDatabase. + * + * @var bool + */ private $autoCreateDatabase; /** - * @var \mysqli + * conn. + * + * @var ?\mysqli */ private $conn; + + /** + * converter. + * + * @var mixed + */ private $converter; + + /** + * generator. + * + * @var mixed + */ private $generator; + + /** + * info. + * + * @var mixed + */ private $info; + + /** + * logger. + * + * @var \vhs\Logger + */ private $logger; + /** + * __construct. + * + * @param \vhs\database\engines\mysql\MySqlConnectionInfo $connectionInfo + * @param ?bool $autoCreateDatabase + * + * @return void + */ public function __construct(MySqlConnectionInfo $connectionInfo, $autoCreateDatabase = false) { $this->info = $connectionInfo; $this->autoCreateDatabase = $autoCreateDatabase; @@ -51,6 +83,14 @@ public function __construct(MySqlConnectionInfo $connectionInfo, $autoCreateData $this->converter = new MySqlConverter(); } + /** + * connect. + * + * @throws \vhs\database\exceptions\DatabaseConnectionException + * @throws \vhs\database\exceptions\DatabaseException + * + * @return bool + */ public function connect() { if (isset($this->conn) && !is_null($this->conn)) { return true; @@ -76,6 +116,11 @@ public function connect() { return true; } + /** + * disconnect. + * + * @return void + */ public function disconnect() { if (isset($this->conn) && !is_null($this->conn)) { $this->conn->close(); @@ -84,10 +129,24 @@ public function disconnect() { unset($this->conn); } + /** + * DateFormat. + * + * @return string + */ public static function DateFormat() { return 'Y-m-d H:i:s'; } + /** + * arbitrary. + * + * @param mixed $command + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return mixed + */ public function arbitrary($command) { $this->logger->log('[ARBITRARY] ' . $command); @@ -102,6 +161,15 @@ public function arbitrary($command) { throw new DatabaseException($this->conn->error); } + /** + * count. + * + * @param \vhs\database\queries\QueryCount $query + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return int|null + */ public function count(QueryCount $query) { $sql = $query->generate($this->generator); @@ -122,6 +190,15 @@ public function count(QueryCount $query) { } } + /** + * delete. + * + * @param \vhs\database\queries\QueryDelete $query + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return bool + */ public function delete(QueryDelete $query) { $sql = $query->generate($this->generator); @@ -134,10 +211,28 @@ public function delete(QueryDelete $query) { throw new DatabaseException($this->conn->error); } + /** + * exists. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return bool + */ public function exists(QuerySelect $query) { return $this->count(new QueryCount($query->table, $query->where, $query->orderBy, $query->limit, $query->offset)) > 0; } + /** + * insert. + * + * @param \vhs\database\queries\QueryInsert $query + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return mixed + */ public function insert(QueryInsert $query) { $sql = $query->generate($this->generator); @@ -150,6 +245,15 @@ public function insert(QueryInsert $query) { throw new DatabaseException($this->conn->error); } + /** + * scalar. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return mixed + */ public function scalar(QuerySelect $query) { $row = $this->select($query); @@ -160,6 +264,15 @@ public function scalar(QuerySelect $query) { return $row[0][$query->columns->all()[0]->name]; } + /** + * select. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return mixed + */ public function select(QuerySelect $query) { $sql = $query->generate($this->generator); @@ -195,10 +308,26 @@ public function select(QuerySelect $query) { return $records; } + /** + * setLogger. + * + * @param \vhs\Logger $logger + * + * @return void + */ public function setLogger(Logger $logger) { $this->logger = $logger; } + /** + * update. + * + * @param \vhs\database\queries\QueryUpdate $query + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return bool + */ public function update(QueryUpdate $query) { $sql = $query->generate($this->generator); diff --git a/vhs/database/engines/mysql/MySqlGenerator.php b/packages/backend-php/vhs/database/engines/mysql/MySqlGenerator.php similarity index 76% rename from vhs/database/engines/mysql/MySqlGenerator.php rename to packages/backend-php/vhs/database/engines/mysql/MySqlGenerator.php index 938c6f332..030e24e63 100644 --- a/vhs/database/engines/mysql/MySqlGenerator.php +++ b/packages/backend-php/vhs/database/engines/mysql/MySqlGenerator.php @@ -30,7 +30,6 @@ use vhs\database\orders\OrderByAscending; use vhs\database\orders\OrderByDescending; use vhs\database\queries\IQueryGenerator; -use vhs\database\queries\Query; use vhs\database\queries\QueryCount; use vhs\database\queries\QueryDelete; use vhs\database\queries\QueryInsert; @@ -52,6 +51,7 @@ use vhs\database\wheres\WhereComparator; use vhs\database\wheres\WhereOr; +/** @typescript */ class MySqlGenerator implements IWhereGenerator, IOrderByGenerator, @@ -62,11 +62,16 @@ class MySqlGenerator implements IJoinGenerator, IOnGenerator, IColumnGenerator { - /** - * @var \mysqli - */ + /** @var \mysqli */ private $conn = null; + /** + * generateAnd. + * + * @param \vhs\database\wheres\WhereAnd $where + * + * @return string + */ public function generateAnd(WhereAnd $where) { $sql = '('; @@ -82,10 +87,25 @@ public function generateAnd(WhereAnd $where) { return $sql; } + /** + * generateAscending. + * + * @param \vhs\database\orders\OrderByAscending $ascending + * + * @return string + */ public function generateAscending(OrderByAscending $ascending) { return $this->gen($ascending, 'ASC'); } + /** + * generateBool. + * + * @param \vhs\database\types\TypeBool $type + * @param bool|null $value + * + * @return string + */ public function generateBool(TypeBool $type, $value = null) { return $this->genVal( function ($val) { @@ -100,10 +120,24 @@ function ($val) { ); } + /** + * generateColumn. + * + * @param \vhs\database\Column $column + * + * @return string + */ public function generateColumn(Column $column) { return "{$column->table->alias}.`{$column->name}`"; } + /** + * generateComparator. + * + * @param \vhs\database\wheres\WhereComparator $where + * + * @return string + */ public function generateComparator(WhereComparator $where) { if ($where->isArray || (is_object($where->value) && get_class($where->value) == 'vhs\\database\\queries\\QuerySelect')) { $sql = $where->column->generate($this); @@ -169,10 +203,25 @@ public function generateComparator(WhereComparator $where) { } } + /** + * generateCross. + * + * @param \vhs\database\joins\JoinCross $join + * + * @return string + */ public function generateCross(JoinCross $join) { return "CROSS JOIN {$join->table->name} {$join->table->alias} ON " . $join->on->generate($this); } + /** + * generateDate. + * + * @param \vhs\database\types\TypeDate $type + * @param string|null $value + * + * @return string + */ public function generateDate(TypeDate $type, $value = null) { return $this->genVal( function ($val) { @@ -183,6 +232,14 @@ function ($val) { ); } + /** + * generateDateTime. + * + * @param \vhs\database\types\TypeDateTime $type + * @param string|null $value + * + * @return string + */ public function generateDateTime(TypeDateTime $type, $value = null) { return $this->genVal( function ($val) { @@ -193,6 +250,13 @@ function ($val) { ); } + /** + * generateDelete. + * + * @param \vhs\database\queries\QueryDelete $query + * + * @return string + */ public function generateDelete(QueryDelete $query) { $clause = !is_null($query->where) ? $query->where->generate($this) : ''; @@ -205,10 +269,27 @@ public function generateDelete(QueryDelete $query) { return $sql; } + /** + * generateDescending. + * + * @param \vhs\database\orders\OrderByDescending $descending + * + * @return string + */ public function generateDescending(OrderByDescending $descending) { return $this->gen($descending, 'DESC'); } + /** + * generateEnum. + * + * @param \vhs\database\types\TypeEnum $type + * @param string|null $value + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return string + */ public function generateEnum(TypeEnum $type, $value = null) { return $this->genVal( function ($val) use ($type) { @@ -225,6 +306,14 @@ function ($val) use ($type) { ); } + /** + * generateFloat. + * + * @param \vhs\database\types\TypeFloat $type + * @param float|null $value + * + * @return string + */ public function generateFloat(TypeFloat $type, $value = null) { return $this->genVal( function ($val) { @@ -235,10 +324,24 @@ function ($val) { ); } + /** + * generateInner. + * + * @param \vhs\database\joins\JoinInner $join + * + * @return string + */ public function generateInner(JoinInner $join) { return "INNER JOIN {$join->table->name} {$join->table->alias} ON " . $join->on->generate($this); } + /** + * generateInsert. + * + * @param \vhs\database\queries\QueryInsert $query + * + * @return string + */ public function generateInsert(QueryInsert $query) { $columns = []; $values = []; @@ -257,6 +360,14 @@ public function generateInsert(QueryInsert $query) { return $sql; } + /** + * generateInt. + * + * @param \vhs\database\types\TypeInt $type + * @param mixed $value + * + * @return string + */ public function generateInt(TypeInt $type, $value = null) { return $this->genVal( function ($val) { @@ -267,30 +378,67 @@ function ($val) { ); } + /** + * generateLeft. + * + * @param \vhs\database\joins\JoinLeft $join + * + * @return string + */ public function generateLeft(JoinLeft $join) { return "LEFT JOIN {$join->table->name} {$join->table->alias} ON " . $join->on->generate($this); } + /** + * generateLimit. + * + * @param \vhs\database\limits\Limit $limit + * + * @return string + */ public function generateLimit(Limit $limit) { $clause = ''; if (isset($limit->limit) && is_numeric($limit->limit)) { $clause = sprintf(' LIMIT %s ', intval($limit->limit)); } + return $clause; } + /** + * generateOffset. + * + * @param \vhs\database\offsets\Offset $offset + * + * @return string + */ public function generateOffset(Offset $offset) { $clause = ''; if (isset($offset->offset) && is_numeric($offset->offset)) { $clause = sprintf(' OFFSET %s ', intval($offset->offset)); } + return $clause; } + /** + * generateOn. + * + * @param \vhs\database\On $on + * + * @return string + */ public function generateOn(On $on) { return '(' . $on->where->generate($this) . ')'; } + /** + * generateOr. + * + * @param \vhs\database\wheres\WhereOr $where + * + * @return string + */ public function generateOr(WhereOr $where) { $sql = '('; @@ -306,14 +454,35 @@ public function generateOr(WhereOr $where) { return $sql; } + /** + * generateOuter. + * + * @param \vhs\database\joins\JoinOuter $join + * + * @return string + */ public function generateOuter(JoinOuter $join) { return "OUTER JOIN {$join->table->name} {$join->table->alias} ON " . $join->on->generate($this); } + /** + * generateRight. + * + * @param \vhs\database\joins\JoinRight $join + * + * @return string + */ public function generateRight(JoinRight $join) { return "RIGHT JOIN {$join->table->name} {$join->table->alias} ON " . $join->on->generate($this); } + /** + * generateSelect. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @return string + */ public function generateSelect(QuerySelect $query) { $selector = implode( ', ', @@ -329,7 +498,7 @@ public function generateSelect(QuerySelect $query) { $sql = "SELECT {$selector} FROM `{$query->table->name}` AS {$query->table->alias}"; if (!is_null($query->joins)) { - /** @var Join $join */ + /** @var \vhs\database\joins\Join $join */ foreach ($query->joins as $join) { $sql .= ' ' . $join->generate($this); } @@ -354,6 +523,13 @@ public function generateSelect(QuerySelect $query) { return $sql; } + /** + * generateSelectCount. + * + * @param \vhs\database\queries\QueryCount $query + * + * @return string + */ public function generateSelectCount(QueryCount $query) { $clause = !is_null($query->where) ? $query->where->generate($this) : ''; $orderClause = !is_null($query->orderBy) ? $query->orderBy->generate($this) : ''; @@ -388,10 +564,21 @@ public function generateSelectCount(QueryCount $query) { return $sql; } + /** + * generateString. + * + * @param \vhs\database\types\TypeString $type + * @param mixed $value + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return string + */ public function generateString(TypeString $type, $value = null) { return $this->genVal( function ($val) use ($type) { $v = (string) $val; + if (strlen($v) > $type->length) { throw new DatabaseException("Value of Type::String exceeds defined length of {$type->length}"); } @@ -403,8 +590,17 @@ function ($val) use ($type) { ); } + /** + * generateText. + * + * @param \vhs\database\types\TypeText $type + * @param mixed $value + * + * @return string + */ public function generateText(TypeText $type, $value = null) { return $this->genVal( + // @phpstan-ignore closure.unusedUse function ($val) use ($type) { return (string) $val; }, @@ -413,6 +609,13 @@ function ($val) use ($type) { ); } + /** + * generateUpdate. + * + * @param \vhs\database\queries\QueryUpdate $query + * + * @return string + */ public function generateUpdate(QueryUpdate $query) { $clause = !is_null($query->where) ? $query->where->generate($this) : ''; $setsql = implode( @@ -420,6 +623,7 @@ public function generateUpdate(QueryUpdate $query) { array_map( function ($columnName, $value) use ($query) { $column = $query->columns->getByName($columnName); + return $column->generate($this) . ' = ' . $column->type->generate($this, $value); }, array_keys($query->values), @@ -442,11 +646,21 @@ function ($columnName, $value) use ($query) { * is on the db to figure escaping. * * @param \mysqli $conn + * + * @return void */ public function SetMySqli(\mysqli $conn) { $this->conn = $conn; } + /** + * gen. + * + * @param OrderBy $orderBy + * @param mixed $type + * + * @return string + */ private function gen(OrderBy $orderBy, $type) { $clause = $orderBy->column->generate($this) . " {$type}, "; @@ -460,6 +674,15 @@ private function gen(OrderBy $orderBy, $type) { return $clause; } + /** + * genVal. + * + * @param callable $gen + * @param Type $type + * @param mixed $value + * + * @return string + */ private function genVal(callable $gen, Type $type, $value = null) { if (is_null($value)) { if ($type->nullable) { diff --git a/vhs/database/exceptions/DatabaseConnectionException.php b/packages/backend-php/vhs/database/exceptions/DatabaseConnectionException.php similarity index 91% rename from vhs/database/exceptions/DatabaseConnectionException.php rename to packages/backend-php/vhs/database/exceptions/DatabaseConnectionException.php index 3bd15cdbb..f11e71fe2 100644 --- a/vhs/database/exceptions/DatabaseConnectionException.php +++ b/packages/backend-php/vhs/database/exceptions/DatabaseConnectionException.php @@ -9,5 +9,6 @@ namespace vhs\database\exceptions; +/** @typescript */ class DatabaseConnectionException extends DatabaseException { } diff --git a/vhs/database/exceptions/DatabaseException.php b/packages/backend-php/vhs/database/exceptions/DatabaseException.php similarity index 90% rename from vhs/database/exceptions/DatabaseException.php rename to packages/backend-php/vhs/database/exceptions/DatabaseException.php index 7600dcbcb..99be7489f 100644 --- a/vhs/database/exceptions/DatabaseException.php +++ b/packages/backend-php/vhs/database/exceptions/DatabaseException.php @@ -9,5 +9,6 @@ namespace vhs\database\exceptions; +/** @typescript */ class DatabaseException extends \Exception { } diff --git a/vhs/database/exceptions/InvalidSchemaException.php b/packages/backend-php/vhs/database/exceptions/InvalidSchemaException.php similarity index 90% rename from vhs/database/exceptions/InvalidSchemaException.php rename to packages/backend-php/vhs/database/exceptions/InvalidSchemaException.php index aaabbc1cb..168130bfa 100644 --- a/vhs/database/exceptions/InvalidSchemaException.php +++ b/packages/backend-php/vhs/database/exceptions/InvalidSchemaException.php @@ -9,5 +9,6 @@ namespace vhs\database\exceptions; +/** @typescript */ class InvalidSchemaException extends \Exception { } diff --git a/packages/backend-php/vhs/database/joins/IJoinGenerator.php b/packages/backend-php/vhs/database/joins/IJoinGenerator.php new file mode 100644 index 000000000..808c5fece --- /dev/null +++ b/packages/backend-php/vhs/database/joins/IJoinGenerator.php @@ -0,0 +1,60 @@ +table = clone $table; + $this->on = $on; + $this->on->__updateTable($this->table); + } + + /** + * Cross. + * + * @param \vhs\database\Table $table + * @param \vhs\database\On $on + * + * @return \vhs\database\joins\JoinCross + */ + public static function Cross(Table $table, On $on) { + return new JoinCross($table, $on); + } + + /** + * Inner. + * + * @param \vhs\database\Table $table + * @param \vhs\database\On $on + * + * @return \vhs\database\joins\JoinInner + */ + public static function Inner(Table $table, On $on) { + return new JoinInner($table, $on); + } + + /** + * Left. + * + * @param \vhs\database\Table $table + * @param \vhs\database\On $on + * + * @return \vhs\database\joins\JoinLeft + */ + public static function Left(Table $table, On $on) { + return new JoinLeft($table, $on); + } + + /** + * Outer. + * + * @param \vhs\database\Table $table + * @param \vhs\database\On $on + * + * @return \vhs\database\joins\JoinOuter + */ + public static function Outer(Table $table, On $on) { + return new JoinOuter($table, $on); + } + + /** + * Right. + * + * @param \vhs\database\Table $table + * @param \vhs\database\On $on + * + * @return \vhs\database\joins\JoinRight + */ + public static function Right(Table $table, On $on) { + return new JoinRight($table, $on); + } + + /** + * generateJoin. + * + * @param \vhs\database\joins\IJoinGenerator $generator + * + * @return mixed + */ + abstract public function generateJoin(IJoinGenerator $generator); + + /** + * generate. + * + * @param \vhs\database\IGenerator $generator + * @param mixed $value + * + * @return mixed + */ + public function generate(IGenerator $generator, $value = null) { + /** @var IJoinGenerator $generator */ + return $this->generateJoin($generator); + } + + /** + * __clone. + * + * @return void + */ + public function __clone(): void { + $this->on = clone $this->on; + } + + /** + * __updateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ + public function __updateTable(Table &$table) { + $this->table = $table; + $this->on->__updateTable($table); + } +} diff --git a/vhs/database/joins/JoinCross.php b/packages/backend-php/vhs/database/joins/JoinCross.php similarity index 93% rename from vhs/database/joins/JoinCross.php rename to packages/backend-php/vhs/database/joins/JoinCross.php index 40995e3e5..e66e0517c 100644 --- a/vhs/database/joins/JoinCross.php +++ b/packages/backend-php/vhs/database/joins/JoinCross.php @@ -9,6 +9,7 @@ namespace vhs\database\joins; +/** @typescript */ class JoinCross extends Join { public function generateJoin(IJoinGenerator $generator) { return $generator->generateCross($this); diff --git a/vhs/database/joins/JoinInner.php b/packages/backend-php/vhs/database/joins/JoinInner.php similarity index 93% rename from vhs/database/joins/JoinInner.php rename to packages/backend-php/vhs/database/joins/JoinInner.php index ce1902af7..b5e9724c8 100644 --- a/vhs/database/joins/JoinInner.php +++ b/packages/backend-php/vhs/database/joins/JoinInner.php @@ -9,6 +9,7 @@ namespace vhs\database\joins; +/** @typescript */ class JoinInner extends Join { public function generateJoin(IJoinGenerator $generator) { return $generator->generateInner($this); diff --git a/vhs/database/joins/JoinLeft.php b/packages/backend-php/vhs/database/joins/JoinLeft.php similarity index 93% rename from vhs/database/joins/JoinLeft.php rename to packages/backend-php/vhs/database/joins/JoinLeft.php index 6099de520..81eb22d58 100644 --- a/vhs/database/joins/JoinLeft.php +++ b/packages/backend-php/vhs/database/joins/JoinLeft.php @@ -9,6 +9,7 @@ namespace vhs\database\joins; +/** @typescript */ class JoinLeft extends Join { public function generateJoin(IJoinGenerator $generator) { return $generator->generateLeft($this); diff --git a/vhs/database/joins/JoinOuter.php b/packages/backend-php/vhs/database/joins/JoinOuter.php similarity index 93% rename from vhs/database/joins/JoinOuter.php rename to packages/backend-php/vhs/database/joins/JoinOuter.php index 50806db46..884242fe0 100644 --- a/vhs/database/joins/JoinOuter.php +++ b/packages/backend-php/vhs/database/joins/JoinOuter.php @@ -9,6 +9,7 @@ namespace vhs\database\joins; +/** @typescript */ class JoinOuter extends Join { public function generateJoin(IJoinGenerator $generator) { return $generator->generateOuter($this); diff --git a/vhs/database/joins/JoinRight.php b/packages/backend-php/vhs/database/joins/JoinRight.php similarity index 93% rename from vhs/database/joins/JoinRight.php rename to packages/backend-php/vhs/database/joins/JoinRight.php index 5e2b3c7d8..d9c9774c6 100644 --- a/vhs/database/joins/JoinRight.php +++ b/packages/backend-php/vhs/database/joins/JoinRight.php @@ -9,6 +9,7 @@ namespace vhs\database\joins; +/** @typescript */ class JoinRight extends Join { public function generateJoin(IJoinGenerator $generator) { return $generator->generateRight($this); diff --git a/vhs/database/limits/ILimitGenerator.php b/packages/backend-php/vhs/database/limits/ILimitGenerator.php similarity index 54% rename from vhs/database/limits/ILimitGenerator.php rename to packages/backend-php/vhs/database/limits/ILimitGenerator.php index a8bb83494..6b5c4b2f8 100644 --- a/vhs/database/limits/ILimitGenerator.php +++ b/packages/backend-php/vhs/database/limits/ILimitGenerator.php @@ -4,6 +4,14 @@ use vhs\database\IGenerator; +/** @typescript */ interface ILimitGenerator extends IGenerator { + /** + * generateLimit. + * + * @param \vhs\database\limits\Limit $limit + * + * @return mixed + */ public function generateLimit(Limit $limit); } diff --git a/vhs/database/limits/Limit.php b/packages/backend-php/vhs/database/limits/Limit.php similarity index 54% rename from vhs/database/limits/Limit.php rename to packages/backend-php/vhs/database/limits/Limit.php index f2ea81edf..b60dcf8fc 100644 --- a/vhs/database/limits/Limit.php +++ b/packages/backend-php/vhs/database/limits/Limit.php @@ -6,22 +6,50 @@ use vhs\database\IGeneratable; use vhs\database\IGenerator; +/** @typescript */ class Limit extends Element implements IGeneratable { + /** @var int|null $limit */ public $limit; + /** + * constructor. + * + * @param int $limit + */ public function __construct($limit) { $this->limit = $limit; } + /** + * Static Limit instantiator. + * + * @param int|null $limit + * + * @return Limit + */ public static function Limit($limit) { return new Limit($limit); } + /** + * generate. + * + * @param ILimitGenerator $generator + * @param mixed $value + * + * @return mixed + */ public function generate(IGenerator $generator, $value = null) { - /** @var ILimitGenerator $generator */ return $this->generateLimit($generator); } + /** + * generateLimit. + * + * @param \vhs\database\limits\ILimitGenerator $generator + * + * @return mixed + */ private function generateLimit(ILimitGenerator $generator) { return $generator->generateLimit($this); } diff --git a/vhs/database/offsets/IOffsetGenerator.php b/packages/backend-php/vhs/database/offsets/IOffsetGenerator.php similarity index 54% rename from vhs/database/offsets/IOffsetGenerator.php rename to packages/backend-php/vhs/database/offsets/IOffsetGenerator.php index d800c7785..0dadec834 100644 --- a/vhs/database/offsets/IOffsetGenerator.php +++ b/packages/backend-php/vhs/database/offsets/IOffsetGenerator.php @@ -4,6 +4,14 @@ use vhs\database\IGenerator; +/** @typescript */ interface IOffsetGenerator extends IGenerator { + /** + * generateOffset. + * + * @param \vhs\database\offsets\Offset $offset + * + * @return mixed + */ public function generateOffset(Offset $offset); } diff --git a/packages/backend-php/vhs/database/offsets/Offset.php b/packages/backend-php/vhs/database/offsets/Offset.php new file mode 100644 index 000000000..d6d7d57cf --- /dev/null +++ b/packages/backend-php/vhs/database/offsets/Offset.php @@ -0,0 +1,61 @@ +offset = $offset; + } + + /** + * Offset. + * + * @param mixed $offset + * + * @return \vhs\database\offsets\Offset + */ + public static function Offset($offset) { + return new Offset($offset); + } + + /** + * generate. + * + * @param \vhs\database\IGenerator $generator + * @param mixed $value + * + * @return mixed + */ + public function generate($generator, $value = null) { + /** @var \vhs\database\offsets\IOffsetGenerator $generator */ + return $this->generateOffset($generator); + } + + /** + * generateOffset. + * + * @param \vhs\database\offsets\IOffsetGenerator $generator + * + * @return mixed + */ + private function generateOffset($generator) { + return $generator->generateOffset($this); + } +} diff --git a/vhs/database/orders/IOrderByGenerator.php b/packages/backend-php/vhs/database/orders/IOrderByGenerator.php similarity index 53% rename from vhs/database/orders/IOrderByGenerator.php rename to packages/backend-php/vhs/database/orders/IOrderByGenerator.php index 2a2cb53b6..e5886be94 100644 --- a/vhs/database/orders/IOrderByGenerator.php +++ b/packages/backend-php/vhs/database/orders/IOrderByGenerator.php @@ -11,8 +11,23 @@ use vhs\database\IGenerator; +/** @typescript */ interface IOrderByGenerator extends IGenerator { + /** + * generateAscending. + * + * @param \vhs\database\orders\OrderByAscending $ascending + * + * @return mixed + */ public function generateAscending(OrderByAscending $ascending); + /** + * generateDescending. + * + * @param \vhs\database\orders\OrderByDescending $descending + * + * @return mixed + */ public function generateDescending(OrderByDescending $descending); } diff --git a/vhs/database/orders/OrderBy.php b/packages/backend-php/vhs/database/orders/OrderBy.php similarity index 60% rename from vhs/database/orders/OrderBy.php rename to packages/backend-php/vhs/database/orders/OrderBy.php index 46fdb7df5..2cb55a05e 100644 --- a/vhs/database/orders/OrderBy.php +++ b/packages/backend-php/vhs/database/orders/OrderBy.php @@ -14,32 +14,76 @@ use vhs\database\IGenerator; use vhs\database\Table; +/** @typescript */ abstract class OrderBy extends Element { /** @var Column */ public $column; /** @var OrderBy[] */ public $orderBy = []; + /** + * constructor. + * + * @param Column $column + * @param OrderBy ...$orderBy + */ public function __construct(Column $column, OrderBy ...$orderBy) { $this->column = $column; $this->orderBy = $orderBy; } + /** + * Ascending. + * + * @param Column $column + * @param OrderBy ...$orderBy + * + * @return OrderByAscending + */ public static function Ascending(Column $column, OrderBy ...$orderBy) { return new OrderByAscending($column, ...$orderBy); } + /** + * Descending. + * + * @param Column $column + * @param OrderBy ...$orderBy + * + * @return OrderByDescending + */ public static function Descending(Column $column, OrderBy ...$orderBy) { return new OrderByDescending($column, ...$orderBy); } + /** + * generateOrderBy. + * + * @param IOrderByGenerator $generator + * + * @return mixed + */ abstract public function generateOrderBy(IOrderByGenerator $generator); + /** + * generate. + * + * @param IOrderByGenerator $generator + * @param mixed $value + * + * @return mixed + */ public function generate(IGenerator $generator, $value = null) { - /** @var IOrderByGenerator $generator */ return $this->generateOrderBy($generator); } + /** + * __updateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ public function __updateTable(Table &$table) { $this->column->__updateTable($table); diff --git a/vhs/database/orders/OrderByAscending.php b/packages/backend-php/vhs/database/orders/OrderByAscending.php similarity index 65% rename from vhs/database/orders/OrderByAscending.php rename to packages/backend-php/vhs/database/orders/OrderByAscending.php index 6ee5d4172..92079dbb1 100644 --- a/vhs/database/orders/OrderByAscending.php +++ b/packages/backend-php/vhs/database/orders/OrderByAscending.php @@ -9,7 +9,15 @@ namespace vhs\database\orders; +/** @typescript */ class OrderByAscending extends OrderBy { + /** + * generateOrderBy. + * + * @param \vhs\database\orders\IOrderByGenerator $generator + * + * @return mixed + */ public function generateOrderBy(IOrderByGenerator $generator) { return $generator->generateAscending($this); } diff --git a/vhs/database/orders/OrderByDescending.php b/packages/backend-php/vhs/database/orders/OrderByDescending.php similarity index 65% rename from vhs/database/orders/OrderByDescending.php rename to packages/backend-php/vhs/database/orders/OrderByDescending.php index 2db417360..b1c15484a 100644 --- a/vhs/database/orders/OrderByDescending.php +++ b/packages/backend-php/vhs/database/orders/OrderByDescending.php @@ -9,7 +9,15 @@ namespace vhs\database\orders; +/** @typescript */ class OrderByDescending extends OrderBy { + /** + * generateOrderBy. + * + * @param \vhs\database\orders\IOrderByGenerator $generator + * + * @return mixed + */ public function generateOrderBy(IOrderByGenerator $generator) { return $generator->generateDescending($this); } diff --git a/packages/backend-php/vhs/database/queries/IQueryGenerator.php b/packages/backend-php/vhs/database/queries/IQueryGenerator.php new file mode 100644 index 000000000..2603c098e --- /dev/null +++ b/packages/backend-php/vhs/database/queries/IQueryGenerator.php @@ -0,0 +1,60 @@ +table = $table; + $this->where = $where; + } + + /** + * Count. + * + * @param \vhs\database\Table $table + * @param \vhs\database\wheres\Where|null $where + * @param \vhs\database\orders\OrderBy|null $orderBy + * @param \vhs\database\limits\Limit|null $limit + * @param \vhs\database\offsets\Offset|null $offset + * + * @return \vhs\database\queries\QueryCount + */ + public static function Count(Table $table, ?Where $where = null, ?OrderBy $orderBy = null, ?Limit $limit = null, ?Offset $offset = null) { + return new QueryCount($table, $where, $orderBy, $limit, $offset); + } + + /** + * Delete. + * + * @param \vhs\database\Table $table + * @param \vhs\database\wheres\Where|null $where + * + * @return \vhs\database\queries\QueryDelete + */ + public static function Delete(Table $table, ?Where $where = null) { + return new QueryDelete($table, $where); + } + + /** + * Insert. + * + * @param \vhs\database\Table $table + * @param \vhs\database\Columns $columns + * @param mixed[] $values + * + * @return \vhs\database\queries\QueryInsert + */ + public static function Insert(Table $table, Columns $columns, array $values) { + return new QueryInsert($table, $columns, $values); + } + + /** + * Select. + * + * @param \vhs\database\Table $table + * @param \vhs\database\Columns|null $columns + * @param \vhs\database\wheres\Where|null $where + * @param \vhs\database\orders\OrderBy|null $orderBy + * @param \vhs\database\limits\Limit|null $limit + * @param \vhs\database\offsets\Offset|null $offset + * + * @return \vhs\database\queries\QuerySelect + */ + public static function Select( + Table $table, + ?Columns $columns = null, + ?Where $where = null, + ?OrderBy $orderBy = null, + ?Limit $limit = null, + ?Offset $offset = null + ) { + return new QuerySelect($table, $columns, $where, $orderBy, $limit, $offset); + } + + /** + * Update. + * + * @param \vhs\database\Table $table + * @param \vhs\database\Columns $columns + * @param \vhs\database\wheres\Where|null $where + * @param mixed[] $values + * + * @return \vhs\database\queries\QueryUpdate + */ + public static function Update(Table $table, Columns $columns, Where $where = null, array $values) { + return new QueryUpdate($table, $columns, $where, $values); + } + + /** + * generateQuery. + * + * @param \vhs\database\queries\IQueryGenerator $generator + * + * @return mixed + */ + abstract public function generateQuery(IQueryGenerator $generator); + + /** + * @param \vhs\database\IGenerator $generator + * @param mixed $value + * + * @return mixed + */ + public function generate(IGenerator $generator, $value = null) { + /** @var IQueryGenerator $generator */ + return $this->generateQuery($generator); + } + + /** + * Join. + * + * @param \vhs\database\joins\Join ...$join + * + * @return self + */ + public function Join(Join ...$join) { + if (is_null($this->joins)) { + $this->joins = []; + } + + array_push($this->joins, ...$join); + + return $this; + } +} diff --git a/vhs/database/queries/QueryCount.php b/packages/backend-php/vhs/database/queries/QueryCount.php similarity index 56% rename from vhs/database/queries/QueryCount.php rename to packages/backend-php/vhs/database/queries/QueryCount.php index 0a724ba53..e1e793df9 100644 --- a/vhs/database/queries/QueryCount.php +++ b/packages/backend-php/vhs/database/queries/QueryCount.php @@ -10,12 +10,14 @@ namespace vhs\database\queries; use vhs\database\Columns; +use vhs\database\IGenerator; use vhs\database\limits\Limit; use vhs\database\offsets\Offset; use vhs\database\orders\OrderBy; use vhs\database\Table; use vhs\database\wheres\Where; +/** @typescript */ class QueryCount extends Query { /** @var Columns */ public $columns; @@ -26,7 +28,16 @@ class QueryCount extends Query { /** @var OrderBy */ public $orderBy; - public function __construct(Table $table, Where $where = null, OrderBy $orderBy = null, Limit $limit = null, Offset $offset = null) { + /** + * |null. + * + * @param Table $table + * @param Where $where + * @param OrderBy $orderBy + * @param Limit $limit + * @param Offset $offset + */ + public function __construct(Table $table, ?Where $where = null, ?OrderBy $orderBy = null, ?Limit $limit = null, ?Offset $offset = null) { parent::__construct($table, $where); $this->orderBy = $orderBy; @@ -34,7 +45,14 @@ public function __construct(Table $table, Where $where = null, OrderBy $orderBy $this->offset = $offset; } - public function generateQuery(IQueryGenerator $generator) { + /** + * generateQuery. + * + * @param \vhs\database\queries\IQueryGenerator $generator + * + * @return mixed + */ + public function generateQuery(IGenerator $generator) { return $generator->generateSelectCount($this); } } diff --git a/vhs/database/queries/QueryDelete.php b/packages/backend-php/vhs/database/queries/QueryDelete.php similarity index 64% rename from vhs/database/queries/QueryDelete.php rename to packages/backend-php/vhs/database/queries/QueryDelete.php index af5a5f448..aa1bb0990 100644 --- a/vhs/database/queries/QueryDelete.php +++ b/packages/backend-php/vhs/database/queries/QueryDelete.php @@ -9,10 +9,15 @@ namespace vhs\database\queries; -use vhs\database\Table; -use vhs\database\wheres\Where; - +/** @typescript */ class QueryDelete extends Query { + /** + * generateQuery. + * + * @param \vhs\database\queries\IQueryGenerator $generator + * + * @return mixed + */ public function generateQuery(IQueryGenerator $generator) { return $generator->generateDelete($this); } diff --git a/vhs/database/queries/QueryInsert.php b/packages/backend-php/vhs/database/queries/QueryInsert.php similarity index 71% rename from vhs/database/queries/QueryInsert.php rename to packages/backend-php/vhs/database/queries/QueryInsert.php index 21aacd6f6..573c6bd5d 100644 --- a/vhs/database/queries/QueryInsert.php +++ b/packages/backend-php/vhs/database/queries/QueryInsert.php @@ -11,13 +11,22 @@ use vhs\database\Columns; use vhs\database\Table; -use vhs\database\wheres\Where; +/** @typescript */ class QueryInsert extends Query { + /** @var Columns */ public $columns; + /** @var mixed */ public $values; - public function __construct(Table $table, Columns $columns, array $values) { + /** + * constructor. + * + * @param Table $table + * @param Columns $columns + * @param mixed $values + */ + public function __construct(Table $table, Columns $columns, $values = null) { parent::__construct($table); $this->columns = $columns; diff --git a/vhs/database/queries/QuerySelect.php b/packages/backend-php/vhs/database/queries/QuerySelect.php similarity index 55% rename from vhs/database/queries/QuerySelect.php rename to packages/backend-php/vhs/database/queries/QuerySelect.php index c3b1c0052..b7486d7e0 100644 --- a/vhs/database/queries/QuerySelect.php +++ b/packages/backend-php/vhs/database/queries/QuerySelect.php @@ -9,6 +9,7 @@ namespace vhs\database\queries; +use vhs\database\Column; use vhs\database\Columns; use vhs\database\limits\Limit; use vhs\database\offsets\Offset; @@ -16,8 +17,9 @@ use vhs\database\Table; use vhs\database\wheres\Where; +/** @typescript */ class QuerySelect extends Query { - /** @var Columns */ + /** @var \vhs\database\Column[]|\vhs\database\Columns */ public $columns; /** @var Limit */ public $limit; @@ -26,13 +28,23 @@ class QuerySelect extends Query { /** @var OrderBy */ public $orderBy; + /** + * constructor. + * + * @param Table $table + * @param Column|Columns|null $columns + * @param Where|null $where + * @param OrderBy|null $orderBy + * @param Limit|null $limit + * @param Offset|null $offset + */ public function __construct( Table $table, - Columns $columns = null, - Where $where = null, - OrderBy $orderBy = null, - Limit $limit = null, - Offset $offset = null + mixed $columns = null, + ?Where $where = null, + ?OrderBy $orderBy = null, + ?Limit $limit = null, + ?Offset $offset = null ) { parent::__construct($table, $where); @@ -42,6 +54,13 @@ public function __construct( $this->offset = $offset; } + /** + * generateQuery. + * + * @param \vhs\database\queries\IQueryGenerator $generator + * + * @return mixed + */ public function generateQuery(IQueryGenerator $generator) { return $generator->generateSelect($this); } diff --git a/vhs/database/queries/QueryUpdate.php b/packages/backend-php/vhs/database/queries/QueryUpdate.php similarity index 63% rename from vhs/database/queries/QueryUpdate.php rename to packages/backend-php/vhs/database/queries/QueryUpdate.php index 810dafeb8..18f179d41 100644 --- a/vhs/database/queries/QueryUpdate.php +++ b/packages/backend-php/vhs/database/queries/QueryUpdate.php @@ -13,11 +13,30 @@ use vhs\database\Table; use vhs\database\wheres\Where; +/** @typescript */ class QueryUpdate extends Query { + /** + * columns. + * + * @var mixed + */ public $columns; + /** + * values. + * + * @var mixed + */ public $values; - public function __construct(Table $table, Columns $columns, Where $where = null, array $values) { + /** + * constructor. + * + * @param Table $table + * @param Columns $columns + * @param Where|null $where + * @param mixed $values + */ + public function __construct(Table $table, Columns $columns, ?Where $where = null, $values = null) { parent::__construct($table, $where); $this->columns = $columns; diff --git a/packages/backend-php/vhs/database/types/ITypeConverter.php b/packages/backend-php/vhs/database/types/ITypeConverter.php new file mode 100644 index 000000000..ac0a21bbf --- /dev/null +++ b/packages/backend-php/vhs/database/types/ITypeConverter.php @@ -0,0 +1,95 @@ +default = $default; } + /** + * Bool. + * + * @param bool $nullable + * @param mixed $default + * + * @return TypeBool + */ public static function Bool($nullable = true, $default = null) { return new TypeBool($nullable, $default); } + /** + * Date. + * + * @param bool $nullable + * @param mixed $default + * + * @return TypeDate + */ public static function Date($nullable = true, $default = null) { return new TypeDate($nullable, $default); } + /** + * DateTime. + * + * @param bool $nullable + * @param mixed $default + * + * @return TypeDateTime + */ public static function DateTime($nullable = true, $default = null) { return new TypeDateTime($nullable, $default); } + /** + * Enum. + * + * @param mixed $values + * + * @return TypeEnum + */ public static function Enum(...$values) { return new TypeEnum(...$values); } + /** + * Float. + * + * @param bool $nullable + * @param mixed $default + * + * @return TypeFloat + */ public static function Float($nullable = true, $default = null) { return new TypeFloat($nullable, $default); } + /** + * Int. + * + * @param bool $nullable + * @param mixed $default + * + * @return TypeInt + */ public static function Int($nullable = true, $default = null) { return new TypeInt($nullable, $default); } + /** + * String. + * + * @param bool $nullable + * @param mixed $default + * @param int $length + * + * @return TypeString + */ public static function String($nullable = true, $default = null, $length = 255) { return new TypeString($nullable, $default, $length); } + /** + * Text. + * + * @param bool $nullable + * @param mixed $default + * + * @return TypeText + */ public static function Text($nullable = true, $default = null) { return new TypeText($nullable, $default); } - abstract public function covertType(ITypeConverter $converter, $value = null); + /** + * convertType. + * + * @param ITypeConverter $converter + * @param mixed $value + * + * @return mixed + */ + abstract public function convertType(ITypeConverter $converter, $value = null); + /** + * generateType. + * + * @param ITypeGenerator $generator + * @param mixed $value + * + * @return mixed + */ abstract public function generateType(ITypeGenerator $generator, $value = null); /** @@ -73,7 +179,7 @@ abstract public function generateType(ITypeGenerator $generator, $value = null); */ public function convert(IConverter $converter, $value = null) { /** @var ITypeConverter $converter */ - return $this->covertType($converter, $value); + return $this->convertType($converter, $value); } /** diff --git a/vhs/database/types/TypeBool.php b/packages/backend-php/vhs/database/types/TypeBool.php similarity index 79% rename from vhs/database/types/TypeBool.php rename to packages/backend-php/vhs/database/types/TypeBool.php index dd0075a2f..b6ba5ba34 100644 --- a/vhs/database/types/TypeBool.php +++ b/packages/backend-php/vhs/database/types/TypeBool.php @@ -9,8 +9,9 @@ namespace vhs\database\types; +/** @typescript */ class TypeBool extends Type { - public function covertType(ITypeConverter $converter, $value = null) { + public function convertType(ITypeConverter $converter, $value = null) { return $converter->convertBool($this, $value); } diff --git a/vhs/database/types/TypeDate.php b/packages/backend-php/vhs/database/types/TypeDate.php similarity index 79% rename from vhs/database/types/TypeDate.php rename to packages/backend-php/vhs/database/types/TypeDate.php index 59ad4c72e..4872d8a85 100644 --- a/vhs/database/types/TypeDate.php +++ b/packages/backend-php/vhs/database/types/TypeDate.php @@ -9,8 +9,9 @@ namespace vhs\database\types; +/** @typescript */ class TypeDate extends Type { - public function covertType(ITypeConverter $converter, $value = null) { + public function convertType(ITypeConverter $converter, $value = null) { return $converter->convertDate($this, $value); } diff --git a/vhs/database/types/TypeDateTime.php b/packages/backend-php/vhs/database/types/TypeDateTime.php similarity index 79% rename from vhs/database/types/TypeDateTime.php rename to packages/backend-php/vhs/database/types/TypeDateTime.php index fd273fe55..a7fb61b0d 100644 --- a/vhs/database/types/TypeDateTime.php +++ b/packages/backend-php/vhs/database/types/TypeDateTime.php @@ -9,8 +9,9 @@ namespace vhs\database\types; +/** @typescript */ class TypeDateTime extends Type { - public function covertType(ITypeConverter $converter, $value = null) { + public function convertType(ITypeConverter $converter, $value = null) { return $converter->convertDateTime($this, $value); } diff --git a/vhs/database/types/TypeEnum.php b/packages/backend-php/vhs/database/types/TypeEnum.php similarity index 61% rename from vhs/database/types/TypeEnum.php rename to packages/backend-php/vhs/database/types/TypeEnum.php index 96495fd90..d9e436cb7 100644 --- a/vhs/database/types/TypeEnum.php +++ b/packages/backend-php/vhs/database/types/TypeEnum.php @@ -11,28 +11,27 @@ use vhs\database\exceptions\InvalidSchemaException; +/** @typescript */ class TypeEnum extends Type { - /** - * @var \string[] - */ - public $values; + /** @var string[] */ + public array $values; /** - * @param \string[] ...$values + * @param string ...$values * - * @throws InvalidSchemaException + * @throws \vhs\database\exceptions\InvalidSchemaException */ - public function __construct(...$values) { - if (is_null($values) || count($values) <= 0) { + public function __construct(string ...$values) { + if (gettype($values) !== 'array' || empty($values)) { throw new InvalidSchemaException('Enums must have at least one value'); } parent::__construct(false, $values[0]); //default value for Enums will also be the first value - $this->values = $values; + $this->values = array_values($values); } - public function covertType(ITypeConverter $converter, $value = null) { + public function convertType(ITypeConverter $converter, $value = null) { return $converter->convertEnum($this, $value); } diff --git a/vhs/database/types/TypeFloat.php b/packages/backend-php/vhs/database/types/TypeFloat.php similarity index 79% rename from vhs/database/types/TypeFloat.php rename to packages/backend-php/vhs/database/types/TypeFloat.php index 9fad7aa31..faf6a4ab0 100644 --- a/vhs/database/types/TypeFloat.php +++ b/packages/backend-php/vhs/database/types/TypeFloat.php @@ -9,8 +9,9 @@ namespace vhs\database\types; +/** @typescript */ class TypeFloat extends Type { - public function covertType(ITypeConverter $converter, $value = null) { + public function convertType(ITypeConverter $converter, $value = null) { return $converter->convertFloat($this, $value); } diff --git a/vhs/database/types/TypeInt.php b/packages/backend-php/vhs/database/types/TypeInt.php similarity index 78% rename from vhs/database/types/TypeInt.php rename to packages/backend-php/vhs/database/types/TypeInt.php index 112fda352..3294fb1eb 100644 --- a/vhs/database/types/TypeInt.php +++ b/packages/backend-php/vhs/database/types/TypeInt.php @@ -9,8 +9,9 @@ namespace vhs\database\types; +/** @typescript */ class TypeInt extends Type { - public function covertType(ITypeConverter $converter, $value = null) { + public function convertType(ITypeConverter $converter, $value = null) { return $converter->convertInt($this, $value); } diff --git a/vhs/database/types/TypeString.php b/packages/backend-php/vhs/database/types/TypeString.php similarity index 69% rename from vhs/database/types/TypeString.php rename to packages/backend-php/vhs/database/types/TypeString.php index ba6213722..86fd8594f 100644 --- a/vhs/database/types/TypeString.php +++ b/packages/backend-php/vhs/database/types/TypeString.php @@ -9,16 +9,25 @@ namespace vhs\database\types; +/** @typescript */ class TypeString extends Type { + /** @var int */ public $length; + /** + * constructor. + * + * @param bool $nullable + * @param mixed $default + * @param int $length + */ public function __construct($nullable = true, $default = null, $length = 255) { parent::__construct($nullable, $default); $this->length = $length; } - public function covertType(ITypeConverter $converter, $value = null) { + public function convertType(ITypeConverter $converter, $value = null) { return $converter->convertString($this, $value); } diff --git a/vhs/database/types/TypeText.php b/packages/backend-php/vhs/database/types/TypeText.php similarity index 79% rename from vhs/database/types/TypeText.php rename to packages/backend-php/vhs/database/types/TypeText.php index 28f41718d..75a86b3b1 100644 --- a/vhs/database/types/TypeText.php +++ b/packages/backend-php/vhs/database/types/TypeText.php @@ -9,8 +9,9 @@ namespace vhs\database\types; +/** @typescript */ class TypeText extends Type { - public function covertType(ITypeConverter $converter, $value = null) { + public function convertType(ITypeConverter $converter, $value = null) { return $converter->convertText($this, $value); } diff --git a/packages/backend-php/vhs/database/wheres/IWhereGenerator.php b/packages/backend-php/vhs/database/wheres/IWhereGenerator.php new file mode 100644 index 000000000..6b3716672 --- /dev/null +++ b/packages/backend-php/vhs/database/wheres/IWhereGenerator.php @@ -0,0 +1,42 @@ +wheres = $where; } + /** + * generateWhere. + * + * @param \vhs\database\wheres\IWhereGenerator $generator + * + * @return mixed + */ public function generateWhere(IWhereGenerator $generator) { return $generator->generateAnd($this); } + /** + * __toString. + * + * @return string + */ public function __toString() { $s = 'WhereAnd('; @@ -35,6 +55,13 @@ public function __toString() { return $s; } + /** + * __updateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ public function __updateTable(Table &$table) { foreach ($this->wheres as $where) { $where->__updateTable($table); diff --git a/vhs/database/wheres/WhereComparator.php b/packages/backend-php/vhs/database/wheres/WhereComparator.php similarity index 59% rename from vhs/database/wheres/WhereComparator.php rename to packages/backend-php/vhs/database/wheres/WhereComparator.php index 40df23407..b6d3f8901 100644 --- a/vhs/database/wheres/WhereComparator.php +++ b/packages/backend-php/vhs/database/wheres/WhereComparator.php @@ -12,17 +12,77 @@ use vhs\database\Column; use vhs\database\Table; +/** @typescript */ class WhereComparator extends Where { - /** @var Column */ + /** + * column. + * + * @var \vhs\database\Column + */ public $column; + + /** + * equal. + * + * @var bool + */ public $equal = true; + + /** + * greater. + * + * @var bool + */ public $greater = false; + + /** + * isArray. + * + * @var bool + */ public $isArray = false; + + /** + * lesser. + * + * @var bool + */ public $lesser = false; + + /** + * like. + * + * @var bool + */ public $like = false; + + /** + * null_compare. + * + * @var bool + */ public $null_compare = false; + + /** + * value. + * + * @var mixed + */ public $value; + /** + * __construct. + * + * @param \vhs\database\Column $column + * @param mixed $value + * @param mixed $null_compare + * @param mixed $equal + * @param mixed $greater + * @param mixed $lesser + * @param mixed $like + * + * @return void + */ public function __construct(Column $column, $value, $null_compare, $equal, $greater, $lesser, $like = false) { $this->column = $column; $this->value = $value; @@ -34,10 +94,22 @@ public function __construct(Column $column, $value, $null_compare, $equal, $grea $this->like = $like; } + /** + * generateWhere. + * + * @param \vhs\database\wheres\IWhereGenerator $generator + * + * @return callable + */ public function generateWhere(IWhereGenerator $generator) { return $generator->generateComparator($this); } + /** + * __toString. + * + * @return string + */ public function __toString() { $s = 'WhereComparator('; @@ -74,6 +146,13 @@ public function __toString() { return $s; } + /** + * __updateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ public function __updateTable(Table &$table) { $this->column->__updateTable($table); } diff --git a/vhs/database/wheres/WhereOr.php b/packages/backend-php/vhs/database/wheres/WhereOr.php similarity index 86% rename from vhs/database/wheres/WhereOr.php rename to packages/backend-php/vhs/database/wheres/WhereOr.php index 8c949f092..b754b3106 100644 --- a/vhs/database/wheres/WhereOr.php +++ b/packages/backend-php/vhs/database/wheres/WhereOr.php @@ -11,6 +11,7 @@ use vhs\database\Table; +/** @typescript */ class WhereOr extends Where { /** @var Where[] */ public $wheres = []; @@ -35,6 +36,13 @@ public function __toString() { return $s; } + /** + * __updateTable. + * + * @param Table $table + * + * @return void + */ public function __updateTable(Table &$table) { foreach ($this->wheres as $where) { $where->__updateTable($table); diff --git a/vhs/domain/Domain.php b/packages/backend-php/vhs/domain/Domain.php similarity index 72% rename from vhs/domain/Domain.php rename to packages/backend-php/vhs/domain/Domain.php index b10654e9e..1d5c938be 100644 --- a/vhs/domain/Domain.php +++ b/packages/backend-php/vhs/domain/Domain.php @@ -16,7 +16,6 @@ use vhs\database\limits\Limit; use vhs\database\offsets\Offset; use vhs\database\orders\OrderBy; -use vhs\database\orders\OrderByAscending; use vhs\database\queries\Query; use vhs\database\wheres\Where; use vhs\domain\collections\ChildDomainCollection; @@ -26,18 +25,61 @@ use vhs\domain\validations\ValidationException; use vhs\domain\validations\ValidationResults; -interface IDomain { - public static function Define(); -} - +/** + * @template T of Domain + * + * @typescript + */ abstract class Domain extends Notifier implements IDomain, \Serializable, \JsonSerializable { + /** + * __definition. + * + * @var mixed + */ private static $__definition = []; + + /** + * __cache. + * + * @var mixed + */ private $__cache; - private $__collections; + + /** + * __collections. + * + * @var array> + */ + private array $__collections; + + /** + * __dirtyChildren. + * + * @var bool + */ private $__dirtyChildren = false; + + /** + * __parentRelationships. + * + * @var mixed + */ private $__parentRelationships; + + /** + * __parentRelationshipsColumnMap. + * + * @var mixed + */ private $__parentRelationshipsColumnMap; + /** + * __construct. + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return void + */ public function __construct() { $schema = self::Schema(); @@ -88,6 +130,7 @@ public function __construct() { foreach ($myFks as $fk) { if ($fk->table === $domain::Schema()->Table()) { $parentFk = $fk; + break; } } @@ -109,6 +152,11 @@ public function __construct() { } } + /** + * AccessDefinition. + * + * @return void + */ public static function AccessDefinition() { $checks = self::Schema()->Table()->checks; @@ -117,29 +165,58 @@ public static function AccessDefinition() { } } - public static function count($filters, array $allowed_columns = null) { + /** + * Coerce filters value from string. + * + * @param string|\vhs\domain\Filter|null $filters + * + * @return void + */ + public static function coerceFilters(&$filters) { + if (is_string($filters)) { + // TODO total hack.. this is to support GET params + $filters = json_decode($filters); + } + } + + /** + * count. + * + * @param string|\vhs\domain\Filter|null $filters + * @param string[]|null $allowed_columns + * + * @return int + */ + public static function count($filters, ?array $allowed_columns = null) { + Domain::coerceFilters($filters); + $where = self::constructFilterWhere($filters, $allowed_columns); return self::doCount($where); } - public static function doCount(Where $where = null) { - $class = get_called_class(); - + /** + * doCount. + * + * @param \vhs\database\wheres\Where|null $where + * + * @return int + */ + public static function doCount(?Where $where = null) { $records = Database::count(Query::Count(self::Schema()->Table(), $where)); return (int) $records; } /** - * @param array $primaryKeyValues + * @param array{id:int}|int $primaryKeyValues * - * @return object + * @return T|null */ public static function find($primaryKeyValues) { + /** @var class-string */ $class = get_called_class(); - /** @var Domain $obj */ $obj = new $class(); if (!$obj->hydrate($primaryKeyValues)) { @@ -150,48 +227,120 @@ public static function find($primaryKeyValues) { } /** - * @return array + * findAll. + * + * @return T[] */ public static function findAll() { return self::hydrateMany(); } + /** + * onAnyBeforeChange. + * + * @param callable $listener + * + * @return void + */ public static function onAnyBeforeChange(callable $listener) { self::staticOn('BeforeChange', $listener); } + /** + * onAnyBeforeCreate. + * + * @param callable $listener + * + * @return void + */ public static function onAnyBeforeCreate(callable $listener) { self::staticOn('BeforeCreate', $listener); } + /** + * onAnyBeforeDelete. + * + * @param callable $listener + * + * @return void + */ public static function onAnyBeforeDelete(callable $listener) { self::staticOn('BeforeDelete', $listener); } + /** + * onAnyBeforeSave. + * + * @param callable $listener + * + * @return void + */ public static function onAnyBeforeSave(callable $listener) { self::staticOn('BeforeSave', $listener); } + /** + * onAnyBeforeUpdate. + * + * @param callable $listener + * + * @return void + */ public static function onAnyBeforeUpdate(callable $listener) { self::staticOn('BeforeUpdate', $listener); } + /** + * onAnyChanged. + * + * @param callable $listener + * + * @return void + */ public static function onAnyChanged(callable $listener) { self::staticOn('Changed', $listener); } + /** + * onAnyCreated. + * + * @param callable $listener + * + * @return void + */ public static function onAnyCreated(callable $listener) { self::staticOn('Created', $listener); } + /** + * onAnyDeleted. + * + * @param callable $listener + * + * @return void + */ public static function onAnyDeleted(callable $listener) { self::staticOn('Deleted', $listener); } + /** + * onAnySaved. + * + * @param callable $listener + * + * @return void + */ public static function onAnySaved(callable $listener) { self::staticOn('Saved', $listener); } + /** + * onAnyUpdated. + * + * @param callable $listener + * + * @return void + */ public static function onAnyUpdated(callable $listener) { self::staticOn('Updated', $listener); } @@ -199,20 +348,22 @@ public static function onAnyUpdated(callable $listener) { /** * Returns a key value pair of data from this domain. * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters - * @param array $allowed_columns + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters + * @param string[]|null $allowed_columns * - * @return array + * @return T[] */ - public static function page($page, $size, $columns, $order, $filters, array $allowed_columns = null) { + public static function page($page, $size, $columns, $order, $filters, ?array $allowed_columns = null) { + Domain::coerceFilters($filters); + $columnNames = explode(',', $columns); $orders = explode(',', $order); - if ($allowed_columns != null && !empty($allowed_columns)) { + if (is_array($allowed_columns) && !empty($allowed_columns)) { $columnNames = array_intersect($allowed_columns, $columnNames); } @@ -254,6 +405,7 @@ public static function page($page, $size, $columns, $order, $filters, array $all $objects = self::where($where, $orderBy, $size, $page); + /** @var T[] */ $retval = []; foreach ($objects as $object) { @@ -274,11 +426,11 @@ public static function page($page, $size, $columns, $order, $filters, array $all } /** - * @param Schema $schema + * @param Schema|null $schema * * @return Schema */ - public static function Schema(Schema $schema = null) { + public static function Schema(?Schema $schema = null) { self::ensureDefined(); $class = get_called_class(); @@ -298,22 +450,39 @@ public static function Type() { } /** - * @param Where $where - * @param OrderBy $orderBy - * @param null $limit - * @param null $offset + * @param \vhs\database\wheres\Where|null $where + * @param \vhs\database\orders\OrderBy|null $orderBy + * @param mixed $limit + * @param mixed $offset * - * @return array + * @return T[] */ - public static function where(Where $where = null, OrderBy $orderBy = null, $limit = null, $offset = null) { + public static function where(?Where $where = null, ?OrderBy $orderBy = null, $limit = null, $offset = null) { return self::hydrateMany($where, $orderBy, $limit, $offset); } + /** + * arbitraryFind. + * + * @param mixed $sql + * + * @return T[] + */ protected static function arbitraryFind($sql) { return self::arbitraryHydrate($sql); } - protected static function hydrateMany(Where $where = null, OrderBy $orderBy = null, $limit = null, $offset = null) { + /** + * hydrateMany. + * + * @param \vhs\database\wheres\Where|null $where + * @param \vhs\database\orders\OrderBy|null $orderBy + * @param int $limit + * @param int $offset + * + * @return T[] + */ + protected static function hydrateMany(?Where $where = null, ?OrderBy $orderBy = null, $limit = null, $offset = null) { $class = get_called_class(); $records = Database::select( @@ -328,11 +497,13 @@ protected static function hydrateMany(Where $where = null, OrderBy $orderBy = nu ); $items = []; + foreach ($records as $row) { - /** @var Domain $obj */ $obj = new $class(); + $obj->setValues($row); $obj->hydrateRelationships(); + array_push($items, $obj); } @@ -340,11 +511,13 @@ protected static function hydrateMany(Where $where = null, OrderBy $orderBy = nu } /** - * @param string $domain - * @param Schema $joinTable - * @param string $as + * @param string $as + * @param string $domain + * @param \vhs\domain\Schema|null $joinTable + * + * @return void */ - protected static function Relationship($as, $domain, Schema $joinTable = null) { + protected static function Relationship($as, $domain, ?Schema $joinTable = null) { self::ensureDefined(); $class = get_called_class(); @@ -354,6 +527,11 @@ protected static function Relationship($as, $domain, Schema $joinTable = null) { self::$__definition[$class]['Relationships'][$as]['JoinTable'] = $joinTable; } + /** + * Relationships. + * + * @return mixed + */ protected static function Relationships() { self::ensureDefined(); @@ -362,17 +540,28 @@ protected static function Relationships() { return self::$__definition[$class]['Relationships']; } + /** + * arbitrary hydrate. + * + * @param mixed $sql + * + * @return T[] + */ private static function arbitraryHydrate($sql) { + /** @var class-string @class */ $class = get_called_class(); $records = Database::arbitrary($sql); + /** @var T[] */ $items = []; + foreach ($records as $row) { - /** @var Domain $obj */ $obj = new $class(); + $obj->setValues($row); $obj->hydrateRelationships(); + array_push($items, $obj); } @@ -390,9 +579,9 @@ private static function arbitraryHydrate($sql) { * } * * @param Columns $columns the filters that are allowed to be used in the filter - * @param $filter + * @param mixed $filter * - * @return null|Where + * @return void|Where|null */ private static function constructFilter(Columns $columns, $filter) { if (is_object($filter)) { @@ -434,12 +623,12 @@ private static function constructFilter(Columns $columns, $filter) { /** * Constructs the WHERE clause for a filter expression. * - * @param $filters - * @param array $allowed_columns either an array of strings containing the list of columns allowed in a filter expression or null which means al columns are allowed + * @param mixed $filters + * @param string[]|null $allowed_columns either an array of strings containing the list of columns allowed in a filter expression or null which means al columns are allowed * - * @return null|Where + * @return Where|null */ - private static function constructFilterWhere($filters, array $allowed_columns = null) { + private static function constructFilterWhere($filters, ?array $allowed_columns = null) { $actualColumns = new Columns(); if ($allowed_columns == null) { @@ -457,6 +646,11 @@ private static function constructFilterWhere($filters, array $allowed_columns = return self::constructFilter($actualColumns, $filters); } + /** + * ensureDefined. + * + * @return void + */ private static function ensureDefined() { $class = get_called_class(); @@ -471,10 +665,15 @@ private static function ensureDefined() { /** * @param ValidationResults $results * - * @return bool + * @return bool|void */ abstract public function validate(ValidationResults &$results); + /** + * delete. + * + * @return void + */ public function delete() { $this->raiseBeforeDelete(); @@ -488,6 +687,11 @@ public function delete() { $this->raiseDeleted(); } + /** + * getInternalData. + * + * @return array + */ public function getInternalData() { $cols = self::Schema()->Columns()->all(); $data = []; @@ -516,58 +720,132 @@ public function getInternalData() { return $data; } + /** + * jsonSerialize. + * + * @return mixed + */ public function jsonSerialize(): mixed { return $this->getInternalData(); } + /** + * onBeforeChange. + * + * @param callable $listener + * + * @return void + */ public function onBeforeChange(callable $listener) { $this->on('BeforeChange', $listener); } + /** + * onBeforeCreate. + * + * @param callable $listener + * + * @return void + */ public function onBeforeCreate(callable $listener) { $this->on('BeforeCreate', $listener); } + /** + * onBeforeDelete. + * + * @param callable $listener + * + * @return void + */ public function onBeforeDelete(callable $listener) { $this->on('BeforeDelete', $listener); } + /** + * onBeforeSave. + * + * @param callable $listener + * + * @return void + */ public function onBeforeSave(callable $listener) { $this->on('BeforeSave', $listener); } + /** + * onBeforeUpdate. + * + * @param callable $listener + * + * @return void + */ public function onBeforeUpdate(callable $listener) { $this->on('BeforeUpdate', $listener); } + /** + * onChanged. + * + * @param callable $listener + * + * @return void + */ public function onChanged(callable $listener) { $this->on('Changed', $listener); } + /** + * onCreated. + * + * @param callable $listener + * + * @return void + */ public function onCreated(callable $listener) { $this->on('Created', $listener); } + /** + * onDeleted. + * + * @param callable $listener + * + * @return void + */ public function onDeleted(callable $listener) { $this->on('Deleted', $listener); } + /** + * onSaved. + * + * @param callable $listener + * + * @return void + */ public function onSaved(callable $listener) { $this->on('Saved', $listener); } + /** + * onUpdated. + * + * @param callable $listener + * + * @return void + */ public function onUpdated(callable $listener) { $this->on('Updated', $listener); } /** - * @param null $validationResults + * @param ValidationResults|null $validationResults * - * @return bool + * @throws \vhs\domain\exceptions\DomainException + * @throws \vhs\domain\validations\ValidationException * - * @throws DomainException - * @throws ValidationException - * @throws \Exception + * @return bool */ public function save(&$validationResults = null) { if (is_null($validationResults)) { @@ -581,6 +859,7 @@ public function save(&$validationResults = null) { if (!$vr->isSuccess()) { if (isset($validationResults)) { $validationResults = $vr; + return false; } else { throw new ValidationException($vr); @@ -607,7 +886,6 @@ public function save(&$validationResults = null) { Database::update(Query::Update(self::Schema()->Table(), self::Schema()->Columns(), $this->pkWhere(), $this->getValues(true))); } - /** @var DomainCollection $collection */ foreach ($this->__collections as $collection) { $collection->save(); } @@ -633,14 +911,33 @@ public function save(&$validationResults = null) { return true; } - public function serialize() { + /** + * serialize. + * + * @return string + */ + public function serialize(): string { return serialize($this->getInternalData()); } - public function unserialize($data) { - //TODO implement + /** + * unserialize. + * + * @param mixed $data + * + * @return void + */ + public function unserialize($data): void { + // TODO implement } + /** + * getValue. + * + * @param string $name + * + * @return mixed + */ protected function getValue($name) { if (self::Schema()->Columns()->contains($name)) { return $this->$name; @@ -649,6 +946,13 @@ protected function getValue($name) { return null; } + /** + * getValues. + * + * @param mixed $excludePrimaryKeys + * + * @return mixed[] + */ protected function getValues($excludePrimaryKeys = false) { $data = []; @@ -679,6 +983,15 @@ protected function getValues($excludePrimaryKeys = false) { return $data; } + /** + * hydrate. + * + * @param mixed $pk + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return bool + */ protected function hydrate($pk = null) { $record = Database::select(Query::Select(self::Schema()->Table(), self::Schema()->Columns(), $this->pkWhere($pk))); @@ -697,56 +1010,117 @@ protected function hydrate($pk = null) { return true; } + /** + * raiseBeforeChange. + * + * @param \vhs\database\Column ...$columns + * + * @return void + */ protected function raiseBeforeChange(Column ...$columns) { $this->raise('BeforeChange', ...$columns); self::staticRaise('BeforeChange', $this, ...$columns); } + /** + * raiseBeforeCreate. + * + * @return void + */ protected function raiseBeforeCreate() { $this->raise('BeforeCreate'); self::staticRaise('BeforeCreate', $this); } + /** + * raiseBeforeDelete. + * + * @return void + */ protected function raiseBeforeDelete() { $this->raise('BeforeDelete'); self::staticRaise('BeforeDelete', $this); } + /** + * raiseBeforeSave. + * + * @return void + */ protected function raiseBeforeSave() { $this->raise('BeforeSave'); self::staticRaise('BeforeSave', $this); } + /** + * raiseBeforeUpdate. + * + * @return void + */ protected function raiseBeforeUpdate() { $this->raise('BeforeUpdate'); self::staticRaise('BeforeUpdate', $this); } + /** + * raiseChanged. + * + * @param \vhs\database\Column ...$columns + * + * @return void + */ protected function raiseChanged(Column ...$columns) { $this->raise('Changed', ...$columns); self::staticRaise('Changed', $this, ...$columns); } + /** + * raiseCreated. + * + * @return void + */ protected function raiseCreated() { $this->raise('Created'); self::staticRaise('Created', $this); } + /** + * raiseDeleted. + * + * @return void + */ protected function raiseDeleted() { $this->raise('Deleted'); self::staticRaise('Deleted', $this); } + /** + * raiseSaved. + * + * @return void + */ protected function raiseSaved() { $this->raise('Saved'); self::staticRaise('Saved', $this); } + /** + * raiseUpdated. + * + * @return void + */ protected function raiseUpdated() { $this->raise('Updated'); self::staticRaise('Updated', $this); } + /** + * setValues. + * + * @param mixed $data + * + * @return void + */ protected function setValues($data) { $cols = []; foreach (self::Schema()->Columns()->all() as $col) { @@ -764,10 +1138,20 @@ protected function setValues($data) { $this->raiseChanged(...$cols); } + /** + * checkIsDirty. + * + * @return bool + */ private function checkIsDirty() { return $this->__cache->hasChanged() || $this->__dirtyChildren; } + /** + * checkIsNew. + * + * @return bool + */ private function checkIsNew() { $pks = self::Schema()->PrimaryKeys(); @@ -779,6 +1163,15 @@ private function checkIsNew() { return $isNew; } + /** + * extractPkValues. + * + * @param mixed $primaryKeyValues + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return string[] + */ private function extractPkValues($primaryKeyValues = null) { $pks = self::Schema()->PrimaryKeys(); @@ -805,8 +1198,15 @@ private function extractPkValues($primaryKeyValues = null) { return $values; } + /** + * hydrateRelationships. + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return void + */ private function hydrateRelationships() { - /** @var DomainCollection $collection */ + /** @var DomainCollection $collection */ foreach ($this->__collections as $collection) { $collection->hydrate(); } @@ -832,6 +1232,13 @@ private function hydrateRelationships() { } } + /** + * pkWhere. + * + * @param mixed $primaryKeyValues + * + * @return \vhs\database\wheres\Where|null + */ private function pkWhere($primaryKeyValues = null) { $values = $this->extractPkValues($primaryKeyValues); @@ -852,6 +1259,13 @@ private function pkWhere($primaryKeyValues = null) { return null; } + /** + * __get. + * + * @param string $name + * + * @return mixed + */ public function __get($name) { $internal = 0 === strpos($name, 'internal_'); if ($internal) { @@ -862,6 +1276,7 @@ public function __get($name) { return $this->$method(); } elseif (self::Schema()->Columns()->contains($name)) { $col = self::Schema()->Columns()->getByName($name); + return $this->__cache->getValue($col->getAbsoluteName()); } elseif (array_key_exists($name, $this->__collections)) { return $this->__collections[$name]; @@ -872,10 +1287,25 @@ public function __get($name) { return null; } + /** + * __serialize. + * + * @return array + */ public function __serialize() { - return serialize($this->getInternalData()); + return $this->getInternalData(); } + /** + * __set. + * + * @param mixed $name + * @param mixed $value + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return void + */ public function __set($name, $value) { $internal = 0 === strpos($name, 'internal_'); if ($internal) { @@ -898,8 +1328,13 @@ public function __set($name, $value) { } } + /** + * __toString. + * + * @return string + */ public function __toString() { - //TODO if the schema has primary keys we could likely simplify and use those. Or even use a hash of the record data + // TODO if the schema has primary keys we could likely simplify and use those. Or even use a hash of the record data $cols = self::Schema()->Columns()->all(); $data = []; foreach ($cols as $col) { @@ -908,10 +1343,23 @@ public function __toString() { } } - return json_encode($data); + $result = json_encode($data); + + if (!is_string($result)) { + throw new \Exception('Failed to convert to string'); + } + + return $result; } + /** + * __unserialize. + * + * @param mixed $data + * + * @return void + */ public function __unserialize($data) { - //TODO implement + // TODO implement } } diff --git a/vhs/domain/DomainValueCache.php b/packages/backend-php/vhs/domain/DomainValueCache.php similarity index 55% rename from vhs/domain/DomainValueCache.php rename to packages/backend-php/vhs/domain/DomainValueCache.php index 5ead2a4fc..5ac1b001e 100644 --- a/vhs/domain/DomainValueCache.php +++ b/packages/backend-php/vhs/domain/DomainValueCache.php @@ -9,29 +9,80 @@ namespace vhs\domain; +/** @typescript */ class DomainValueCache { + /** + * cache. + * + * @var mixed + */ private $cache; + + /** + * changed. + * + * @var bool + */ private $changed = false; + + /** + * keys. + * + * @var mixed + */ private $keys; + /** + * __construct. + * + * @param mixed $keys + * + * @return void + */ public function __construct($keys) { $this->keys = $keys; $this->cache = array_fill_keys($this->keys, null); } + /** + * clear. + * + * @return void + */ public function clear() { unset($this->cache); $this->cache = array_fill_keys($this->keys, null); } + /** + * getValue. + * + * @param mixed $name + * + * @return mixed + */ public function getValue($name) { return $this->cache[$name]; } + /** + * hasChanged. + * + * @return bool + */ public function hasChanged() { return $this->changed; } + /** + * setValue. + * + * @param mixed $name + * @param mixed $value + * @param mixed $silent + * + * @return void + */ public function setValue($name, $value, $silent = false) { $this->changed = $this->changed || !$silent; diff --git a/packages/backend-php/vhs/domain/Filter.php b/packages/backend-php/vhs/domain/Filter.php new file mode 100644 index 000000000..c48e8f2e7 --- /dev/null +++ b/packages/backend-php/vhs/domain/Filter.php @@ -0,0 +1,198 @@ +left = $left; + $this->column = $column; + $this->operator = $operator; + $this->right = $right; + $this->value = $value; + } + + /** + * _And. + * + * @param mixed $left + * @param mixed $right + * + * @return \vhs\domain\Filter + */ + public static function _And($left, $right) { + return new Filter($left, null, 'and', $right, null); + } + + /** + * _Or. + * + * @param mixed $left + * @param mixed $right + * + * @return \vhs\domain\Filter + */ + public static function _Or($left, $right) { + return new Filter($left, null, 'or', $right, null); + } + + /** + * Equal. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function Equal($column, $value) { + return new Filter(null, $column, '=', null, $value); + } + + /** + * Greater. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function Greater($column, $value) { + return new Filter(null, $column, '>', null, $value); + } + + /** + * GreaterEqual. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function GreaterEqual($column, $value) { + return new Filter(null, $column, '>=', null, $value); + } + + /** + * IsNotNull. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function IsNotNull($column, $value) { + return new Filter(null, $column, 'not null', null, null); + } + + /** + * IsNull. + * + * @param mixed $column + * + * @return \vhs\domain\Filter + */ + public static function IsNull($column) { + return new Filter(null, $column, 'is null', null, null); + } + + /** + * Lesser. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function Lesser($column, $value) { + return new Filter(null, $column, '<', null, $value); + } + + /** + * LesserEqual. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function LesserEqual($column, $value) { + return new Filter(null, $column, '<=', null, $value); + } + + /** + * Like. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function Like($column, $value) { + return new Filter(null, $column, 'like', null, $value); + } + + /** + * NotEqual. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function NotEqual($column, $value) { + return new Filter(null, $column, '!=', null, $value); + } +} diff --git a/packages/backend-php/vhs/domain/IDomain.php b/packages/backend-php/vhs/domain/IDomain.php new file mode 100644 index 000000000..a69330103 --- /dev/null +++ b/packages/backend-php/vhs/domain/IDomain.php @@ -0,0 +1,13 @@ +__ensureListeners($event); array_push($this->__listeners[$event], $listener); } + /** + * raise. + * + * @param mixed $event + * @param mixed ...$args + * + * @return void + */ protected function raise($event, ...$args) { $this->__ensureListeners($event); @@ -35,6 +71,14 @@ protected function raise($event, ...$args) { } } + /** + * staticRaise. + * + * @param mixed $event + * @param mixed ...$args + * + * @return void + */ protected function staticRaise($event, ...$args) { self::__ensureStaticListeners($event); @@ -45,6 +89,13 @@ protected function staticRaise($event, ...$args) { } } + /** + * __ensureListeners. + * + * @param mixed $event + * + * @return void + */ private function __ensureListeners($event) { if (is_null($this->__listeners)) { $this->__listeners = []; @@ -55,6 +106,13 @@ private function __ensureListeners($event) { } } + /** + * __ensureStaticListeners. + * + * @param mixed $event + * + * @return void + */ private static function __ensureStaticListeners($event) { if (is_null(self::$__staticListeners)) { self::$__staticListeners = []; diff --git a/vhs/domain/Schema.php b/packages/backend-php/vhs/domain/Schema.php similarity index 68% rename from vhs/domain/Schema.php rename to packages/backend-php/vhs/domain/Schema.php index b3b9deb26..5e4de4c65 100644 --- a/vhs/domain/Schema.php +++ b/packages/backend-php/vhs/domain/Schema.php @@ -12,29 +12,39 @@ use vhs\database\Columns; use vhs\database\constraints\ForeignKey; use vhs\database\constraints\PrimaryKey; -use vhs\database\Table; -interface ISchema { +/** @typescript */ +abstract class Schema implements ISchema { /** - * @return Table + * internal_table. + * + * @var mixed */ - public static function init(); -} - -abstract class Schema implements ISchema { private $internal_table; + /** + * __construct. + */ protected function __construct() { $this->internal_table = $this->init(); } /** - * @return Table + * Table. + * + * @return \vhs\database\Table */ public static function &Table() { return self::getInstance()->internal_table; } + /** + * Column. + * + * @param mixed $name + * + * @return \vhs\database\Column + */ public static function Column($name) { return self::Table()->columns->$name; } @@ -42,16 +52,32 @@ public static function Column($name) { /** * @return Columns */ + /** + * Columns. + * + * @return \vhs\database\Columns + */ public static function Columns() { return self::Table()->columns; } + /** + * Constraints. + * + * @return \vhs\database\constraints\Constraint[] + */ public static function Constraints() { return self::Table()->constraints; } + /** + * ForeignKeys. + * + * @return \vhs\database\constraints\ForeignKey[] + */ public static function ForeignKeys() { $fks = []; + foreach (self::Table()->constraints as $constraint) { if ($constraint instanceof ForeignKey) { array_push($fks, $constraint); @@ -62,7 +88,9 @@ public static function ForeignKeys() { } /** - * @return PrimaryKey[] + * PrimaryKeys. + * + * @return \vhs\database\constraints\PrimaryKey[] */ public static function PrimaryKeys() { $pks = []; @@ -76,14 +104,18 @@ public static function PrimaryKeys() { } /** - * @return Schema + * Type. + * + * @return \vhs\domain\Schema */ final public static function Type() { return self::getInstance(); } /** - * @return Schema + * getInstance. + * + * @return \vhs\domain\Schema */ private static function getInstance() { static $aoInstance = []; @@ -97,6 +129,11 @@ private static function getInstance() { return $aoInstance[$class]; } - private function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { } } diff --git a/vhs/domain/collections/ChildDomainCollection.php b/packages/backend-php/vhs/domain/collections/ChildDomainCollection.php similarity index 76% rename from vhs/domain/collections/ChildDomainCollection.php rename to packages/backend-php/vhs/domain/collections/ChildDomainCollection.php index a528c8464..56be63c40 100644 --- a/vhs/domain/collections/ChildDomainCollection.php +++ b/packages/backend-php/vhs/domain/collections/ChildDomainCollection.php @@ -11,22 +11,61 @@ use vhs\database\Column; use vhs\database\constraints\ForeignKey; -use vhs\database\constraints\PrimaryKey; use vhs\database\wheres\Where; use vhs\domain\Domain; use vhs\domain\exceptions\DomainException; +/** + * @template T of Domain + * + * @extends DomainCollection + * + * @typescript + */ class ChildDomainCollection extends DomainCollection { - /** @var Column $childColumn */ + /** + * childColumn. + * + * @var \vhs\database\Column + */ private $childColumn; - /** @var PrimaryKey $childKey */ + + /** + * childKey. + * + * @var \vhs\database\constraints\PrimaryKey + */ private $childKey; + + /** + * childType. + * + * @var mixed + */ private $childType; - /** @var Domain $parent */ + + /** + * parent. + * + * @var \vhs\domain\Domain + */ private $parent; - /** @var Column $parentColumn */ + + /** + * parentColumn. + * + * @var Column $parentColumn + */ private $parentColumn; + /** + * __construct. + * + * @param \vhs\domain\Domain $parent + * @param mixed $childType + * + * @throws \vhs\domain\exceptions\DomainException + */ public function __construct(Domain $parent, $childType) { $this->parent = $parent; $this->childType = $childType; @@ -55,11 +94,18 @@ public function __construct(Domain $parent, $childType) { throw new DomainException('Child relationship incomplete - missing referenced child and/or parent column on joined tables'); } - //TODO something with before deletes - maybe not because this is a direct relationship + // TODO something with before deletes - maybe not because this is a direct relationship $this->clear(); } + /** + * add. + * + * @param \vhs\domain\Domain $item + * + * @return void + */ public function add(Domain $item) { $this->raiseBeforeAdd(); if ($this->contains($item)) { @@ -80,6 +126,11 @@ public function add(Domain $item) { $this->raiseAdded(); } + /** + * all. + * + * @return \vhs\domain\Domain[] + */ public function all() { $childPkName = $this->childKey->column->name; @@ -92,22 +143,49 @@ public function all() { return $all; } + /** + * clear. + * + * @return void + */ public function clear() { $this->__existing = []; $this->__new = []; $this->__removed = []; } + /** + * compare. + * + * @param \vhs\domain\Domain $a + * @param \vhs\domain\Domain $b + * + * @return bool + */ public function compare(Domain $a, Domain $b) { $childPkName = $this->childKey->column->name; + return $a->$childPkName === $b->$childPkName; } + /** + * contains. + * + * @param \vhs\domain\Domain $item + * + * @return bool + */ public function contains(Domain $item) { $childPkName = $this->childKey->column->name; + return $this->containsKey($item->$childPkName); } + /** + * hydrate. + * + * @return void + */ public function hydrate() { $this->clear(); @@ -122,6 +200,13 @@ public function hydrate() { } } + /** + * remove. + * + * @param \vhs\domain\Domain $item + * + * @return void + */ public function remove(Domain $item) { $this->raiseBeforeRemove(); if ($this->contains($item)) { @@ -140,6 +225,11 @@ public function remove(Domain $item) { } } + /** + * save. + * + * @return void + */ public function save() { $this->raiseBeforeSave(); diff --git a/packages/backend-php/vhs/domain/collections/DomainCollection.php b/packages/backend-php/vhs/domain/collections/DomainCollection.php new file mode 100644 index 000000000..7ff4a5cb5 --- /dev/null +++ b/packages/backend-php/vhs/domain/collections/DomainCollection.php @@ -0,0 +1,243 @@ + $item + * + * @return void + */ + abstract public function add(Domain $item); + + /** + * all. + * + * @return \vhs\domain\Domain[] + */ + abstract public function all(); + + /** + * compare. + * + * @param \vhs\domain\Domain $a + * @param \vhs\domain\Domain $b + * + * @return bool + */ + abstract public function compare(Domain $a, Domain $b); + + /** + * contains. + * + * @param \vhs\domain\Domain $item + * + * @return bool + */ + abstract public function contains(Domain $item); + + /** + * hydrate. + * + * @return void + */ + abstract public function hydrate(); + + /** + * remove. + * + * @param \vhs\domain\Domain $item + * + * @return void + */ + abstract public function remove(Domain $item); + + /** + * save. + * + * @return void + */ + abstract public function save(); + + /** + * clear. + * + * @return void + */ + public function clear() { + $this->__existing = []; + $this->__new = []; + $this->__removed = []; + } + + /** + * containsKey. + * + * @param string $key + * + * @return bool + */ + public function containsKey($key) { + return array_key_exists($key, $this->all()); + } + + /** + * onAdded. + * + * @param callable $listener + * + * @return void + */ + public function onAdded(callable $listener) { + $this->on('Added', $listener); + } + + /** + * onBeforeAdd. + * + * @param callable $listener + * + * @return void + */ + public function onBeforeAdd(callable $listener) { + $this->on('BeforeAdd', $listener); + } + + /** + * onBeforeRemove. + * + * @param callable $listener + * + * @return void + */ + public function onBeforeRemove(callable $listener) { + $this->on('BeforeRemove', $listener); + } + + /** + * onBeforeSave. + * + * @param callable $listener + * + * @return void + */ + public function onBeforeSave(callable $listener) { + $this->on('BeforeSave', $listener); + } + + /** + * onRemoved. + * + * @param callable $listener + * + * @return void + */ + public function onRemoved(callable $listener) { + $this->on('Removed', $listener); + } + + /** + * onSaved. + * + * @param callable $listener + * + * @return void + */ + public function onSaved(callable $listener) { + $this->on('Saved', $listener); + } + + /** + * raiseAdded. + * + * @return void + */ + protected function raiseAdded() { + $this->raise('Added'); + } + + /** + * raiseBeforeAdd. + * + * @return void + */ + protected function raiseBeforeAdd() { + $this->raise('BeforeUpdate'); + } + + /** + * raiseBeforeRemove. + * + * @return void + */ + protected function raiseBeforeRemove() { + $this->raise('BeforeDelete'); + } + + /** + * raiseBeforeSave. + * + * @return void + */ + protected function raiseBeforeSave() { + $this->raise('BeforeSave'); + } + + /** + * raiseRemoved. + * + * @return void + */ + protected function raiseRemoved() { + $this->raise('Removed'); + } + + /** + * raiseSaved. + * + * @return void + */ + protected function raiseSaved() { + $this->raise('Saved'); + } +} diff --git a/vhs/domain/collections/ParentDomainCollection.php b/packages/backend-php/vhs/domain/collections/ParentDomainCollection.php similarity index 53% rename from vhs/domain/collections/ParentDomainCollection.php rename to packages/backend-php/vhs/domain/collections/ParentDomainCollection.php index b12f350df..a06309bcc 100644 --- a/vhs/domain/collections/ParentDomainCollection.php +++ b/packages/backend-php/vhs/domain/collections/ParentDomainCollection.php @@ -11,39 +11,104 @@ use vhs\domain\Domain; +/** + * @template T of Domain + * + * @extends DomainCollection + * + * @typescript + */ class ParentDomainCollection extends DomainCollection { + /** + * add. + * + * @param \vhs\domain\Domain $item + * + * @return void + */ public function add(Domain $item) { // TODO: Implement add() method. $this->raiseBeforeAdd(); $this->raiseAdded(); } + /** + * all. + * + * @return \vhs\domain\Domain[] + */ public function all() { // TODO: Implement all() method. + return []; } + /** + * compare. + * + * @param \vhs\domain\Domain $a + * @param \vhs\domain\Domain $b + * + * @return bool + */ public function compare(Domain $a, Domain $b) { // TODO: Implement compare() method. + + return false; } + /** + * contains. + * + * @param \vhs\domain\Domain $item + * + * @return bool + */ public function contains(Domain $item) { // TODO: Implement contains() method. + return false; } + /** + * containsKey. + * + * @param int $key + * + * @return bool + * + * @phpstan-ignore method.childParameterType + */ public function containsKey($key) { // TODO: Implement containsKey() method. + return false; } + /** + * hydrate. + * + * @return void + */ public function hydrate() { // TODO: Implement hydrate() method. } + /** + * remove. + * + * @param \vhs\domain\Domain $item + * + * @return void + */ public function remove(Domain $item) { // TODO: Implement remove() method. $this->raiseBeforeRemove(); $this->raiseRemoved(); } + /** + * save. + * + * @return void + */ public function save() { // TODO: Implement save() method. $this->raiseBeforeSave(); diff --git a/vhs/domain/collections/SatelliteDomainCollection.php b/packages/backend-php/vhs/domain/collections/SatelliteDomainCollection.php similarity index 77% rename from vhs/domain/collections/SatelliteDomainCollection.php rename to packages/backend-php/vhs/domain/collections/SatelliteDomainCollection.php index fb0833868..cb6353835 100644 --- a/vhs/domain/collections/SatelliteDomainCollection.php +++ b/packages/backend-php/vhs/domain/collections/SatelliteDomainCollection.php @@ -10,7 +10,6 @@ namespace vhs\domain\collections; use vhs\database\Columns; -use vhs\database\constraints\ForeignKey; use vhs\database\Database; use vhs\database\queries\Query; use vhs\database\wheres\Where; @@ -18,15 +17,58 @@ use vhs\domain\exceptions\DomainException; use vhs\domain\Schema; +/** + * @template T of Domain + * + * @extends DomainCollection + * + * @typescript + */ class SatelliteDomainCollection extends DomainCollection { - /** @var ForeignKey */ + /** + * childKey. + * + * @var \vhs\database\constraints\ForeignKey + */ private $childKey; + + /** + * childType. + * + * @var mixed + */ private $childType; + + /** + * joinTable. + * + * @var \vhs\domain\Schema + */ private $joinTable; + + /** + * parent. + * + * @var \vhs\domain\Domain + */ private $parent; - /** @var ForeignKey */ + + /** + * parentKey. + * + * @var \vhs\database\constraints\ForeignKey + */ private $parentKey; + /** + * __construct. + * + * @param \vhs\domain\Domain $parent + * @param mixed $childType + * @param \vhs\domain\Schema $joinTable + * + * @throws \vhs\domain\exceptions\DomainException + */ public function __construct(Domain $parent, $childType, Schema $joinTable) { $this->parent = $parent; $this->childType = $childType; @@ -59,6 +101,15 @@ public function __construct(Domain $parent, $childType, Schema $joinTable) { $this->clear(); } + /** + * add. + * + * @param \vhs\domain\Domain $item + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return void + */ public function add(Domain $item) { $this->raiseBeforeAdd(); @@ -76,6 +127,11 @@ public function add(Domain $item) { $this->raiseAdded(); } + /** + * all. + * + * @return \vhs\domain\Domain[] + */ public function all() { $childOnCol = $this->childKey->on->name; @@ -88,16 +144,38 @@ public function all() { return $all; } + /** + * compare. + * + * @param \vhs\domain\Domain $a + * @param \vhs\domain\Domain $b + * + * @return bool + */ public function compare(Domain $a, Domain $b) { $childOnCol = $this->childKey->on->name; + return $a->$childOnCol === $b->$childOnCol; } + /** + * contains. + * + * @param \vhs\domain\Domain $item + * + * @return bool + */ public function contains(Domain $item) { $childOnCol = $this->childKey->on->name; + return $this->containsKey($item->$childOnCol); } + /** + * hydrate. + * + * @return void + */ public function hydrate() { $this->clear(); @@ -120,12 +198,19 @@ public function hydrate() { array_push($childIds, $row[$this->childKey->column->name]); } + /** @var Domain $childType */ $childType = $this->childType; - /** @var Domain $childType */ $this->__existing = $childType::where(Where::In($this->childKey->on, $childIds)); } + /** + * remove. + * + * @param \vhs\domain\Domain $item + * + * @return void + */ public function remove(Domain $item) { $this->raiseBeforeRemove(); if ($this->contains($item)) { @@ -140,6 +225,11 @@ public function remove(Domain $item) { } } + /** + * save. + * + * @return void + */ public function save() { $this->raiseBeforeSave(); diff --git a/vhs/domain/exceptions/DomainException.php b/packages/backend-php/vhs/domain/exceptions/DomainException.php similarity index 90% rename from vhs/domain/exceptions/DomainException.php rename to packages/backend-php/vhs/domain/exceptions/DomainException.php index ca00380dc..32516a672 100644 --- a/vhs/domain/exceptions/DomainException.php +++ b/packages/backend-php/vhs/domain/exceptions/DomainException.php @@ -9,5 +9,6 @@ namespace vhs\domain\exceptions; +/** @typescript */ class DomainException extends \Exception { } diff --git a/vhs/domain/exceptions/InvalidColumnDefinitionException.php b/packages/backend-php/vhs/domain/exceptions/InvalidColumnDefinitionException.php similarity index 91% rename from vhs/domain/exceptions/InvalidColumnDefinitionException.php rename to packages/backend-php/vhs/domain/exceptions/InvalidColumnDefinitionException.php index a3b55e26f..5118b71a4 100644 --- a/vhs/domain/exceptions/InvalidColumnDefinitionException.php +++ b/packages/backend-php/vhs/domain/exceptions/InvalidColumnDefinitionException.php @@ -9,5 +9,6 @@ namespace vhs\domain\exceptions; +/** @typescript */ class InvalidColumnDefinitionException extends DomainException { } diff --git a/vhs/domain/validations/ValidationException.php b/packages/backend-php/vhs/domain/validations/ValidationException.php similarity index 70% rename from vhs/domain/validations/ValidationException.php rename to packages/backend-php/vhs/domain/validations/ValidationException.php index db4e9becf..7d8d5e61b 100644 --- a/vhs/domain/validations/ValidationException.php +++ b/packages/backend-php/vhs/domain/validations/ValidationException.php @@ -9,9 +9,22 @@ namespace vhs\domain\validations; +/** @typescript */ class ValidationException extends \Exception { + /** + * results. + * + * @var \vhs\domain\validations\ValidationResults + */ private $results; + /** + * __construct. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ public function __construct(ValidationResults $results) { $this->results = $results; @@ -25,6 +38,8 @@ public function __construct(ValidationResults $results) { } /** + * getResults. + * * @return ValidationResults */ public function getResults() { diff --git a/vhs/domain/validations/ValidationFailure.php b/packages/backend-php/vhs/domain/validations/ValidationFailure.php similarity index 58% rename from vhs/domain/validations/ValidationFailure.php rename to packages/backend-php/vhs/domain/validations/ValidationFailure.php index 9befb302b..0c81ade13 100644 --- a/vhs/domain/validations/ValidationFailure.php +++ b/packages/backend-php/vhs/domain/validations/ValidationFailure.php @@ -9,13 +9,31 @@ namespace vhs\domain\validations; +/** @typescript */ class ValidationFailure { + /** + * message. + * + * @var string + */ private $message; + /** + * __construct. + * + * @param string $message + * + * @return void + */ public function __construct($message) { $this->message = $message; } + /** + * getMessage. + * + * @return string + */ public function getMessage() { return $this->message; } diff --git a/vhs/domain/validations/ValidationResults.php b/packages/backend-php/vhs/domain/validations/ValidationResults.php similarity index 60% rename from vhs/domain/validations/ValidationResults.php rename to packages/backend-php/vhs/domain/validations/ValidationResults.php index 40a51938b..9b4d28daa 100644 --- a/vhs/domain/validations/ValidationResults.php +++ b/packages/backend-php/vhs/domain/validations/ValidationResults.php @@ -9,24 +9,38 @@ namespace vhs\domain\validations; +/** @typescript */ class ValidationResults { + /** + * failures. + * + * @var \vhs\domain\validations\ValidationFailure[] + */ private $failures = []; /** - * @param ValidationFailure $failure + * add. + * + * @param \vhs\domain\validations\ValidationFailure $failure + * + * @return void */ public function add(ValidationFailure $failure) { array_push($this->failures, $failure); } /** - * @return ValidationFailure[] + * getFailures. + * + * @return \vhs\domain\validations\ValidationFailure[] */ public function getFailures() { return $this->failures; } /** + * isSuccess. + * * @return bool */ public function isSuccess() { diff --git a/packages/backend-php/vhs/exceptions/HttpException.php b/packages/backend-php/vhs/exceptions/HttpException.php new file mode 100644 index 000000000..c304be2dd --- /dev/null +++ b/packages/backend-php/vhs/exceptions/HttpException.php @@ -0,0 +1,25 @@ +value, $previous); + } +} diff --git a/packages/backend-php/vhs/gateways/Engine.php b/packages/backend-php/vhs/gateways/Engine.php new file mode 100644 index 000000000..df08e20f9 --- /dev/null +++ b/packages/backend-php/vhs/gateways/Engine.php @@ -0,0 +1,175 @@ +>> + */ + private $gatewayInstances = []; + + /** + * gatewaysPrefix. + * + * @var string + */ + private $gatewaysPrefix = 'vhs\gateways'; + + protected function __construct() { + parent::__construct(); + + $this->discover(); + } + + /** + * Get the default gateway for a particular category and type. + * + * E.g. \vhs\gateways\Engine::getInstance()->getDefaultGateway('messages', 'email') will get the default email gateway. + * + * @param string $gatewayCategory + * @param string $gatewayType + * + * @return object + */ + public function getDefaultGateway($gatewayCategory, $gatewayType): object { + if (!isset($this->gatewayInstances[$gatewayCategory][$gatewayType]['default'])) { + throw new \Exception(sprintf('No default gateway found for %s - %s', $gatewayCategory, $gatewayType)); + } + + return $this->getNamedGateway($gatewayCategory, $gatewayType, $this->gatewayInstances[$gatewayCategory][$gatewayType]['default']); + } + + /** + * Get a named gateway implementation for a particular category and type. + * + * E.g. \vhs\gateways\Engine::getInstance()->getDefaultGateway('messages', 'email', 'AWSSESClient') will get the AWSSESClient. + * + * @param string $gatewayCategory + * @param string $gatewayType + * @param string $className + * + * @return object + */ + public function getNamedGateway($gatewayCategory, $gatewayType, $className): object { + if (!isset($this->gatewayInstances[$gatewayCategory][$gatewayType][$className])) { + throw new \Exception(sprintf('No named gateway found for %s/%s/%s', $gatewayCategory, $gatewayType, $className)); + } + + return $this->gatewayInstances[$gatewayCategory][$gatewayType][$className]; + } + + /** + * Set default gateway implementation for a particular category and type. + * + * I.e. \vhs\gateways\Engine::getInstance()->setDefaultGateway('messages', 'email', 'AWSSESClient') will set AWSSESClient as the default email gateway. + * + * @param string $gatewayCategory + * @param string $gatewayType + * @param string $className + * + * @return void + */ + public function setDefaultGateway($gatewayCategory, $gatewayType, $className): void { + if (!isset($this->gatewayInstances[$gatewayCategory][$gatewayType][$className])) { + throw new \Exception(sprintf('No named gateway found for %s/%s/%s', $gatewayCategory, $gatewayType, $className)); + } + + $this->gatewayInstances[$gatewayCategory][$gatewayType]['default'] = $className; + } + + /** + * Magic auto-discovery and registration. + * + * @return void + */ + protected function discover(): void { + $gatewayFiles = $this->scanAllDir(__DIR__); + + foreach ($gatewayFiles as $gatewayFile) { + $gatewayDefinition = explode(DIRECTORY_SEPARATOR, str_replace('.php', '', $gatewayFile)); + + $this->register($gatewayDefinition[0], $gatewayDefinition[1], $gatewayDefinition[2]); + } + } + + /** + * Automagically register a class instance of a gateway. + * + * @param mixed $gatewayCategory The top-level category. E.g. 'messages' + * @param mixed $gatewayType The sub-level type. E.g. 'email' + * @param mixed $className The class name of the actual implementation. E.g. 'AWSSESClient' + * @param bool $autoDefault Whether to automatically set the class as the default handler for that category/type. Defaults to true. + * + * @return void + */ + protected function register($gatewayCategory, $gatewayType, $className, $autoDefault = true): void { + $interfacePath = sprintf('%s\interfaces\I%s%sGateway', $this->gatewaysPrefix, ucfirst($gatewayCategory), ucfirst($gatewayType)); + $classPath = sprintf('%s\%s\%s\%s', $this->gatewaysPrefix, $gatewayCategory, $gatewayType, $className); + + $gatewayClass = new \ReflectionClass($classPath); + + if ( + !in_array($interfacePath, $gatewayClass->getInterfaceNames()) && + !in_array(sprintf('%s\interfaces\IGateway', $this->gatewaysPrefix), $gatewayClass->getInterfaceNames()) + ) { + throw new \Exception( + sprintf( + 'Gateway (%s) does not implement required interface %s. Found: [%s]', + $classPath, + $interfacePath, + implode(',', $gatewayClass->getInterfaceNames()) + ) + ); + } + + if ($gatewayClass->getParentClass()->getName() !== 'vhs\Loggington') { + throw new \Exception( + sprintf('Gateway (%s) does not extend vhs\Loggington. Found: [%s]', $classPath, $gatewayClass->getParentClass()->getName()) + ); + } + + $this->gatewayInstances[$gatewayCategory][$gatewayType][$className] = call_user_func(sprintf('%s::getInstance', $classPath)); + + $this->gatewayInstances[$gatewayCategory][$gatewayType][$className]->setLogger($this->logger); + + if ($autoDefault && !isset($this->gatewayInstances[$gatewayCategory][$gatewayType]['default'])) { + $this->gatewayInstances[$gatewayCategory][$gatewayType]['default'] = $className; + } + } + + /** + * scanAllDir. + * + * @param string $scanDir + * + * @return string[] + */ + private function scanAllDir($scanDir) { + $result = []; + + foreach (scandir($scanDir) as $fileName) { + if (!preg_match('/^(\.\.?|interfaces|Engine.php)/', $fileName)) { + $filePath = $scanDir . DIRECTORY_SEPARATOR . $fileName; + + if (is_dir($filePath)) { + foreach ($this->scanAllDir($filePath) as $childFilename) { + $result[] = $fileName . DIRECTORY_SEPARATOR . $childFilename; + } + } else { + $result[] = $fileName; + } + } + } + + return $result; + } +} diff --git a/packages/backend-php/vhs/gateways/interfaces/IGateway.php b/packages/backend-php/vhs/gateways/interfaces/IGateway.php new file mode 100644 index 000000000..b982e572a --- /dev/null +++ b/packages/backend-php/vhs/gateways/interfaces/IGateway.php @@ -0,0 +1,7 @@ +client = new SesClient([ + 'region' => AWS_SES_REGION, + 'credentials' => [ + 'key' => AWS_SES_CLIENT_ID, + 'secret' => AWS_SES_SECRET + ] + ]); + } + + public function health(): bool { + return true; + } + + /** + * Send a rich (text+html) email. + * + * @param string|string[] $recipients Recipient address or addresses array + * @param string $subject Email subject + * @param mixed $textContent text content + * @param mixed $htmlContent html content + * + * @return bool + */ + public function sendRichEmail(string|array $recipients, string $subject, $textContent, $htmlContent): bool { + try { + $this->client->sendEmail([ + 'Source' => NOMOS_FROM_EMAIL, + 'Destination' => [ + 'ToAddresses' => is_array($recipients) ? $recipients : [$recipients] + ], + 'Message' => [ + 'Subject' => [ + // Data is required + 'Data' => $subject + ], + // Body is required + 'Body' => [ + 'Text' => [ + // Data is required + 'Data' => $textContent + ], + 'Html' => [ + // Data is required + 'Data' => $htmlContent + ] + ] + ] + ]); + + return true; + } catch (\Exception $e) { + $this->logger->log('An unknown error occured while trying to sendRichEmail: ' . $e->getMessage()); + + return false; + } + } + + /** + * Send a simple (plain text) email. + * + * @param string|string[] $recipients Recipient address or addresses array + * @param string $subject Email subject + * @param mixed $textContent text content + * + * @return bool + */ + public function sendSimpleEmail(string|array $recipients, string $subject, $textContent): bool { + try { + $this->client->sendEmail([ + 'Source' => NOMOS_FROM_EMAIL, + 'Destination' => [ + 'ToAddresses' => is_array($recipients) ? $recipients : [$recipients] + ], + 'Message' => [ + 'Subject' => [ + // Data is required + 'Data' => $subject + ], + // Body is required + 'Body' => [ + 'Text' => [ + // Data is required + 'Data' => $textContent + ] + ] + ] + ]); + + return true; + } catch (\Exception $e) { + $this->logger->log('An unknown error occured while trying to sendSimpleEmail: ' . $e->getMessage()); + + return false; + } + } +} diff --git a/vhs/loggers/ConsoleLogger.php b/packages/backend-php/vhs/loggers/ConsoleLogger.php similarity index 72% rename from vhs/loggers/ConsoleLogger.php rename to packages/backend-php/vhs/loggers/ConsoleLogger.php index a87fdb277..eff28922d 100644 --- a/vhs/loggers/ConsoleLogger.php +++ b/packages/backend-php/vhs/loggers/ConsoleLogger.php @@ -11,7 +11,15 @@ use vhs\Logger; +/** @typescript */ class ConsoleLogger extends Logger { + /** + * log. + * + * @param mixed $message + * + * @return void + */ public function log($message) { print '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL; } diff --git a/vhs/loggers/FileLogger.php b/packages/backend-php/vhs/loggers/FileLogger.php similarity index 59% rename from vhs/loggers/FileLogger.php rename to packages/backend-php/vhs/loggers/FileLogger.php index be57fbff3..8ab8c6b32 100644 --- a/vhs/loggers/FileLogger.php +++ b/packages/backend-php/vhs/loggers/FileLogger.php @@ -12,14 +12,40 @@ use vhs\Logger; use vhs\security\CurrentUser; +/** @typescript */ class FileLogger extends Logger { + /** + * file. + * + * @var mixed + */ private $file; + + /** + * filename. + * + * @var string + */ private $filename; + /** + * __construct. + * + * @param string $filename + * + * @return void + */ public function __construct($filename) { $this->filename = $filename; } + /** + * log. + * + * @param string $message + * + * @return void + */ public function log($message) { $this->ensureFile(); if (is_resource($this->file)) { @@ -27,9 +53,18 @@ public function log($message) { } } + /** + * ensureFile. + * + * @return void + */ private function ensureFile() { if (!isset($this->file)) { $this->file = fopen($this->filename, 'a'); + + if (!is_resource($this->file)) { + throw new \Exception('Failed to open log file'); + } } } } diff --git a/vhs/loggers/SilentLogger.php b/packages/backend-php/vhs/loggers/SilentLogger.php similarity index 67% rename from vhs/loggers/SilentLogger.php rename to packages/backend-php/vhs/loggers/SilentLogger.php index 725a90c52..db7623e54 100644 --- a/vhs/loggers/SilentLogger.php +++ b/packages/backend-php/vhs/loggers/SilentLogger.php @@ -11,7 +11,15 @@ use vhs\Logger; +/** @typescript */ class SilentLogger extends Logger { + /** + * log. + * + * @param mixed $message + * + * @return void + */ public function log($message) { //stfu } diff --git a/vhs/loggers/StringLogger.php b/packages/backend-php/vhs/loggers/StringLogger.php similarity index 65% rename from vhs/loggers/StringLogger.php rename to packages/backend-php/vhs/loggers/StringLogger.php index 8716da2e4..f4e6152fb 100644 --- a/vhs/loggers/StringLogger.php +++ b/packages/backend-php/vhs/loggers/StringLogger.php @@ -11,17 +11,31 @@ use vhs\Logger; +/** @typescript */ class StringLogger extends Logger { - public $history; + /** @var string[] */ + public array $history; public function __construct() { $this->history = []; } + /** + * fullText. + * + * @return string + */ public function fullText() { return implode("\n", $this->history); } + /** + * log. + * + * @param mixed $message + * + * @return void + */ public function log($message) { array_push($this->history, $message); } diff --git a/vhs/migration/Backup.php b/packages/backend-php/vhs/migration/Backup.php similarity index 73% rename from vhs/migration/Backup.php rename to packages/backend-php/vhs/migration/Backup.php index 41bbc64fe..502f8a5e0 100644 --- a/vhs/migration/Backup.php +++ b/packages/backend-php/vhs/migration/Backup.php @@ -12,14 +12,55 @@ use vhs\Logger; use vhs\loggers\SilentLogger; +/** @typescript */ class Backup { + /** + * database. + * + * @var string + */ private $database; + + /** + * logger. + * + * @var \vhs\Logger + */ private $logger; + + /** + * password. + * + * @var string + */ private $password; + + /** + * server. + * + * @var string + */ private $server; + + /** + * user. + * + * @var string + */ private $user; - public function __construct($server, $user, $password, $database, Logger $logger = null) { + /** + * __construct. + * + * @param string $server + * @param string $user + * @param string $password + * @param string $database + * @param \vhs\Logger $logger + * + * @return void + */ + public function __construct($server, $user, $password, $database, ?Logger $logger = null) { $this->server = $server; $this->user = $user; $this->password = $password; @@ -32,33 +73,48 @@ public function __construct($server, $user, $password, $database, Logger $logger } } + /** + * external_backup. + * + * @param bool $do_host + * @param string $fileName + * @param string $backupPath + * + * @return bool + */ public function external_backup($do_host = false, $fileName = null, $backupPath = 'backup/') { $this->logger->log('Starting backup'); - $fileName = !is_null($fileName) ? $fileName : 'db-backup-' . time() . '.sql'; + $fileName = !is_null($fileName) ? $fileName : sprintf('db-backup-%s.sql', time()); $command = []; $command[] = 'mysqldump'; - $command[] = "-u '" . $this->user . "'"; - $command[] = '-p' . $this->password; $command[] = '--ssl=0'; $command[] = '--no-tablespaces'; // workaround for breaking change introduced in minor mysql version - see https://dba.stackexchange.com/questions/271981/access-denied-you-need-at-least-one-of-the-process-privileges-for-this-ope + $command[] = sprintf("-u '%s'", $this->user); + $command[] = sprintf("-p'%s'", $this->password); if ($do_host == true) { - $command[] = '--host ' . $this->server; + $command[] = '--host'; + $command[] = $this->server; + $command[] = '--skip-ssl-verify-server-cert'; } - $command[] = "'" . $this->database . "'"; + $command[] = sprintf("'%s'", $this->database); $command[] = '>'; - $command[] = "'" . $backupPath . $fileName . "'"; + $command[] = sprintf("'%s%s'", $backupPath, $fileName); exec(implode(' ', $command), $output, $return); - if (!$return) { - return true; - } else { - return false; - } + return !$return; } + /** + * internal_backup. + * + * @param string $fileName + * @param string $backupPath + * + * @return bool + */ public function internal_backup($fileName = null, $backupPath = 'backup/') { $this->logger->log('Starting backup'); @@ -66,6 +122,7 @@ public function internal_backup($fileName = null, $backupPath = 'backup/') { if ($conn->connect_error) { $this->logger->log('Connection failed: ' . $conn->connect_error); + return false; } @@ -93,9 +150,11 @@ public function internal_backup($fileName = null, $backupPath = 'backup/') { print 'Grabbing table ' . $table . ': '; $create_result = $conn->query($sql); - if (!$result) { + + if (!$create_result) { return false; } + $create_row = $create_result->fetch_row(); $return .= $create_row[1]; @@ -105,6 +164,7 @@ public function internal_backup($fileName = null, $backupPath = 'backup/') { if (!$result) { return false; } + $num_fields = @intval($result->num_rows); $return .= ";\nINSERT INTO `" . $table . '` VALUES('; @@ -151,6 +211,13 @@ public function internal_backup($fileName = null, $backupPath = 'backup/') { return true; } + /** + * is_mysql_num. + * + * @param mixed $type + * + * @return bool + */ private function is_mysql_num($type) { //http://php.net/manual/en/mysqli-result.fetch-field.php @@ -160,13 +227,13 @@ private function is_mysql_num($type) { case 3: //INTEGER case 4: //FLOAT case 5: //DOUBLE - //case 7: //TIMESTAMP - not an int, but treated similar here + //case 7: //TIMESTAMP - not an int, but treated similar here case 8: //BIGINT / SERIAL case 9: //MEDIUMINT case 16: //BIT case 246: //DECIMAL / NUMERIC / FIXED return true; - break; + default: return false; } diff --git a/vhs/migration/Migrator.php b/packages/backend-php/vhs/migration/Migrator.php similarity index 75% rename from vhs/migration/Migrator.php rename to packages/backend-php/vhs/migration/Migrator.php index ec8f337e0..dd5d35410 100644 --- a/vhs/migration/Migrator.php +++ b/packages/backend-php/vhs/migration/Migrator.php @@ -12,14 +12,61 @@ use vhs\Logger; use vhs\loggers\SilentLogger; +/** @typescript */ class Migrator { + /** + * command options. + * + * @var string[] + */ + private array $cmd_opts = []; + /** + * database. + * + * @var string + */ private $database; + + /** + * logger. + * + * @var \vhs\Logger + */ private $logger; + + /** + * password. + * + * @var string + */ private $password; + + /** + * server. + * + * @var string + */ private $server; + + /** + * user. + * + * @var string + */ private $user; - public function __construct($server, $user, $password, $database, Logger $logger = null) { + /** + * __construct. + * + * @param string $server + * @param string $user + * @param string $password + * @param string $database + * @param \vhs\Logger $logger + * + * @return void + */ + public function __construct($server, $user, $password, $database, ?Logger $logger = null) { $this->server = $server; $this->user = $user; $this->password = $password; @@ -30,8 +77,20 @@ public function __construct($server, $user, $password, $database, Logger $logger } else { $this->logger = $logger; } + + if (getenv('MIGRATOR_SKIP_SSL', true) !== false) { + array_push($this->cmd_opts, '--skip-ssl'); + } } + /** + * migrate. + * + * @param int|null $toVersion + * @param string $migrationsPath + * + * @return bool + */ public function migrate($toVersion = null, $migrationsPath = '.') { $this->logger->log('Starting migration'); @@ -39,6 +98,7 @@ public function migrate($toVersion = null, $migrationsPath = '.') { if ($conn->connect_error) { $this->logger->log('Connection failed: ' . $conn->connect_error); + return false; } @@ -47,6 +107,7 @@ public function migrate($toVersion = null, $migrationsPath = '.') { $this->logger->log('Database created successfully'); } else { $this->logger->log('Error creating database: ' . $conn->error); + return false; } @@ -73,17 +134,20 @@ public function migrate($toVersion = null, $migrationsPath = '.') { if ($toVersion > $versions[sizeof($versions) - 1]) { $this->logger->log('Cannot target a version higher than latest.'); + return false; } if ($toVersion <= $currentversion) { $this->logger->log('Target must be higher than current.'); + return false; } } if ($currentversion == $versions[sizeof($versions) - 1]) { $this->logger->log('Already up to date.'); + return true; } @@ -98,14 +162,22 @@ public function migrate($toVersion = null, $migrationsPath = '.') { continue; } - //TODO these should prob be in a transaction to allow rollback in case a migration fails. + // TODO these should prob be in a transaction to allow rollback in case a migration fails. $this->logger->log('Upgrading to: ' . $version); $scripts = array_values(array_diff(scandir($migrationsPath . '/' . $version, SCANDIR_SORT_ASCENDING), ['..', '.'])); $script_path = $migrationsPath . '/' . $version . '/'; - $command = 'mysql -u' . DB_USER . ' -p' . DB_PASS . ' ' . '-h ' . DB_SERVER . ' -D ' . DB_DATABASE . " --ssl=0 < {$script_path}"; + $command = sprintf( + "mysql -u %s -p%s -h %s -D %s %s --ssl=0 < %s", + DB_USER, + DB_PASS, + DB_SERVER, + DB_DATABASE, + implode(' ', $this->cmd_opts), + $script_path + ); foreach ($scripts as $script) { $this->logger->log('Executing: ' . $script); @@ -114,6 +186,7 @@ public function migrate($toVersion = null, $migrationsPath = '.') { $output = shell_exec($command . $script); } catch (\Exception $e) { $this->logger->log('Caught exception: ' . $e->getMessage() . "\n"); + return false; } diff --git a/packages/backend-php/vhs/monitors/Monitor.php b/packages/backend-php/vhs/monitors/Monitor.php new file mode 100644 index 000000000..834a0b0dd --- /dev/null +++ b/packages/backend-php/vhs/monitors/Monitor.php @@ -0,0 +1,29 @@ +token = $token; } + /** + * getToken. + * + * @return string + */ public function getToken() { return $this->token; } diff --git a/vhs/security/CurrentUser.php b/packages/backend-php/vhs/security/CurrentUser.php similarity index 64% rename from vhs/security/CurrentUser.php rename to packages/backend-php/vhs/security/CurrentUser.php index 5031908e5..8bf420fc6 100644 --- a/vhs/security/CurrentUser.php +++ b/packages/backend-php/vhs/security/CurrentUser.php @@ -9,56 +9,105 @@ namespace vhs\security; -use vhs\Singleton; - +/** @typescript */ class CurrentUser { + /** @var \vhs\security\IPrincipal $currentPrincipal */ + public $currentPrincipal; + /** - * @var IPrincipal $currentPrincipal + * __construct. */ - private $currentPrincipal; - protected function __construct() { $this->currentPrincipal = new AnonPrincipal(); } + /** + * canGrantAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ final public static function canGrantAllPermissions(...$permission) { return CurrentUser::getPrincipal()->canGrantAllPermissions(...$permission); } + /** + * canGrantAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ final public static function canGrantAnyPermissions(...$permission) { return CurrentUser::getPrincipal()->canGrantAnyPermissions(...$permission); } + /** + * getIdentity. + * + * @return mixed + */ final public static function getIdentity() { return CurrentUser::getPrincipal()->getIdentity(); } /** - * @return IPrincipal + * getPrincipal. + * + * @return \vhs\security\IPrincipal */ final public static function getPrincipal() { return CurrentUser::getInstance()->currentPrincipal; } + /** + * hasAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ final public static function hasAllPermissions(...$permission) { return CurrentUser::getPrincipal()->hasAllPermissions(...$permission); } + /** + * hasAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ final public static function hasAnyPermissions(...$permission) { return CurrentUser::getPrincipal()->hasAnyPermissions(...$permission); } + /** + * isAnon. + * + * @return bool + */ final public static function isAnon() { return CurrentUser::getPrincipal()->isAnon(); } /** - * @param IPrincipal $principal + * setPrincipal. + * + * @param \vhs\security\IPrincipal $principal + * + * @return void */ final public static function setPrincipal($principal) { CurrentUser::getInstance()->currentPrincipal = $principal; } + /** + * getInstance. + * + * @return \vhs\security\CurrentUser + */ final protected static function getInstance() { static $aoInstance = []; @@ -77,6 +126,11 @@ final protected static function getInstance() { } } - private function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { } } diff --git a/vhs/security/IAuthenticate.php b/packages/backend-php/vhs/security/IAuthenticate.php similarity index 62% rename from vhs/security/IAuthenticate.php rename to packages/backend-php/vhs/security/IAuthenticate.php index b7ba2de69..fba83ef88 100644 --- a/vhs/security/IAuthenticate.php +++ b/packages/backend-php/vhs/security/IAuthenticate.php @@ -9,9 +9,10 @@ namespace vhs\security; +/** @typescript */ interface IAuthenticate { /** - * @return IPrincipal + * @return \vhs\security\IPrincipal */ public static function currentPrincipal(); @@ -20,7 +21,19 @@ public static function currentPrincipal(); */ public static function isAuthenticated(); + /** + * login. + * + * @param \vhs\security\ICredentials $credentials + * + * @return void + */ public static function login(ICredentials $credentials); + /** + * logout. + * + * @return void + */ public static function logout(); } diff --git a/vhs/security/ICredentials.php b/packages/backend-php/vhs/security/ICredentials.php similarity index 88% rename from vhs/security/ICredentials.php rename to packages/backend-php/vhs/security/ICredentials.php index b7a935140..e0c34e57f 100644 --- a/vhs/security/ICredentials.php +++ b/packages/backend-php/vhs/security/ICredentials.php @@ -9,5 +9,6 @@ namespace vhs\security; +/** @typescript */ interface ICredentials { } diff --git a/packages/backend-php/vhs/security/IPrincipal.php b/packages/backend-php/vhs/security/IPrincipal.php new file mode 100644 index 000000000..9b6aba305 --- /dev/null +++ b/packages/backend-php/vhs/security/IPrincipal.php @@ -0,0 +1,74 @@ +username = $username; $this->password = $password; } + /** + * getPassword. + * + * @return string + */ public function getPassword() { return $this->password; } + /** + * getUsername. + * + * @return string + */ public function getUsername() { return $this->username; } diff --git a/packages/backend-php/vhs/security/exceptions/InvalidCredentials.php b/packages/backend-php/vhs/security/exceptions/InvalidCredentials.php new file mode 100644 index 000000000..f6a89ac29 --- /dev/null +++ b/packages/backend-php/vhs/security/exceptions/InvalidCredentials.php @@ -0,0 +1,20 @@ +context = $context; + } + + /** + * Shared wrapper to throw a DomainException when a service handler domain class is not found. + * + * @throws \vhs\exceptions\HttpException + * + * @return void + */ + protected function throwNotFound() { + $className = get_called_class(); + + throw new HttpException(sprintf('%s not found', $className), HttpStatusCodes::Client_Error_Not_Found); + } +} diff --git a/vhs/services/ServiceClient.php b/packages/backend-php/vhs/services/ServiceClient.php similarity index 74% rename from vhs/services/ServiceClient.php rename to packages/backend-php/vhs/services/ServiceClient.php index 5eb0cf7b3..b5ca1cb1e 100644 --- a/vhs/services/ServiceClient.php +++ b/packages/backend-php/vhs/services/ServiceClient.php @@ -9,7 +9,16 @@ namespace vhs\services; +/** @typescript */ class ServiceClient { + /** + * __callStatic. + * + * @param string $name + * @param mixed $arguments + * + * @return mixed + */ public static function __callStatic($name, $arguments) { $uri = explode('_', $name); @@ -17,7 +26,7 @@ public static function __callStatic($name, $arguments) { $service = $uri[1]; $method = $uri[2]; - $uri = '/services/' . $namespace . '/' . $service . '.svc'; + $uri = "/services/$namespace/$service.svc"; $contract = ServiceRegistry::get($namespace)->discover($uri, true); diff --git a/vhs/services/ServiceContext.php b/packages/backend-php/vhs/services/ServiceContext.php similarity index 62% rename from vhs/services/ServiceContext.php rename to packages/backend-php/vhs/services/ServiceContext.php index e9046d444..1d72077d2 100644 --- a/vhs/services/ServiceContext.php +++ b/packages/backend-php/vhs/services/ServiceContext.php @@ -11,14 +11,33 @@ use vhs\services\endpoints\Endpoint; +/** @typescript */ class ServiceContext { - /** @var Endpoint $endpoint */ + /** + * endpoint. + * + * @var \vhs\services\endpoints\Endpoint + */ public $endpoint; + /** + * __construct. + * + * @param \vhs\services\endpoints\Endpoint $endpoint + * + * @return void + */ public function __construct(Endpoint $endpoint) { $this->endpoint = $endpoint; } + /** + * log. + * + * @param mixed $message + * + * @return void + */ public function log($message) { if (is_null($this->endpoint->logger)) { return; diff --git a/vhs/services/ServiceHandler.php b/packages/backend-php/vhs/services/ServiceHandler.php similarity index 72% rename from vhs/services/ServiceHandler.php rename to packages/backend-php/vhs/services/ServiceHandler.php index 85f063fd6..6547f1549 100644 --- a/vhs/services/ServiceHandler.php +++ b/packages/backend-php/vhs/services/ServiceHandler.php @@ -14,14 +14,31 @@ use vhs\services\exceptions\InvalidRequestException; use vhs\SplClassLoader; use vhs\SplClassLoaderItem; +use vhs\web\enums\HttpStatusCodes; +/** + * ServiceHandler. + * + * @typescript + */ class ServiceHandler { + /** @var string */ private $endpointNamespace; /** @var Logger */ private $logger; + /** @var string */ private $rootNamespacePath; + /** @var string|null */ private $uriPrefixPath; + /** + * constructor. + * + * @param \vhs\Logger &$logger + * @param string $endpointNamespace + * @param string|null $rootNamespacePath + * @param string|null $uriPrefixPath + */ public function __construct(Logger &$logger, $endpointNamespace, $rootNamespacePath = null, $uriPrefixPath = null) { $this->logger = &$logger; $this->endpointNamespace = $endpointNamespace; @@ -31,6 +48,14 @@ public function __construct(Logger &$logger, $endpointNamespace, $rootNamespaceP SplClassLoader::getInstance()->add(new SplClassLoaderItem($this->endpointNamespace, $this->rootNamespacePath, '.svc.php')); } + /** + * discover. + * + * @param string $uri + * @param bool $isNative + * + * @return mixed + */ public function discover($uri, $isNative = false) { /** @var Endpoint $endpoint */ $endpoint = $this->getEndpoint($uri); @@ -45,7 +70,9 @@ public function discover($uri, $isNative = false) { } /** - * @return Endpoint[] + * Get all endpoints. + * + * @return string[] */ public function getAllEndpoints() { $files = scandir($this->rootNamespacePath . '/' . str_replace('\\', '/', $this->endpointNamespace)); @@ -54,7 +81,7 @@ public function getAllEndpoints() { foreach ($files as $file) { if (preg_match('%(?P.*)\.svc.php%im', $file, $matches)) { - /** @var Endpoint $endpoint */ + /** @var class-string $endpoint */ $endpoint = $this->endpointNamespace . '\\' . $matches['endpoint']; array_push($endpoints, $endpoint::getInstance()); } @@ -63,10 +90,23 @@ public function getAllEndpoints() { return $endpoints; } + /** + * handle. + * + * @param string $uri + * @param mixed $data + * @param bool $isNative + * + * @throws \vhs\services\exceptions\InvalidRequestException + * + * @return mixed + */ public function handle($uri, $data = null, $isNative = false) { /** @var Endpoint[] $endpoints */ $endpoints = []; + $this->logger->debug(__FILE__, __LINE__, __METHOD__, sprintf('handling: %s with prefixpath %s', $uri, $this->uriPrefixPath)); + if (!preg_match('%.*/' . $this->uriPrefixPath . '(?P.*)\.svc/(?P.*)%im', $uri, $regs)) { if (!preg_match('%.*/' . $this->uriPrefixPath . '(?P.*)\.svc%im', $uri, $regs)) { if (preg_match('%.*/' . $this->uriPrefixPath . 'help%im', $uri, $regs)) { @@ -78,16 +118,20 @@ public function handle($uri, $data = null, $isNative = false) { } } } else { - throw new InvalidRequestException('Invalid service request'); + $this->logger->debug(__FILE__, __LINE__, __METHOD__, sprintf('did not find match for: %s', $uri)); + + throw new InvalidRequestException('Invalid service request', HttpStatusCodes::Client_Error_Misdirected_Request); } } } + $this->logger->debug(__FILE__, __LINE__, __METHOD__, sprintf('$endpoints => %s', json_encode($endpoints))); + if (count($endpoints) > 0) { $discovery = []; foreach ($endpoints as $class) { - /** @var Endpoint $endpoint */ + /** @var class-string $endpoint */ $endpoint = $this->endpointNamespace . '\\' . $class; $discovery[$class . '.svc'] = $endpoint::getInstance()->deserializeOutput($endpoint::getInstance()->discover()); @@ -100,7 +144,7 @@ public function handle($uri, $data = null, $isNative = false) { */ return json_encode($discovery); } else { - /** @var Endpoint $endpoint */ + /** @var class-string $endpoint */ $endpoint = $this->endpointNamespace . '\\' . $regs['endpoint']; if (array_key_exists('method', $regs)) { @@ -126,19 +170,19 @@ public function handle($uri, $data = null, $isNative = false) { } /** - * @param $uri + * getEndpoint. * - * @return Endpoint + * @param string $uri * - * @throws InvalidRequestException + * @throws \vhs\services\exceptions\InvalidRequestException + * + * @return string */ private function getEndpoint($uri) { if (!preg_match('%.*/' . $this->uriPrefixPath . '(?P.*)\.svc%im', $uri, $regs)) { - throw new InvalidRequestException('Invalid service request'); + throw new InvalidRequestException('Invalid endpoint request', HttpStatusCodes::Client_Error_Im_a_teapot); } - $endpoint = $this->endpointNamespace . '\\' . $regs['endpoint']; - - return $endpoint; + return $this->endpointNamespace . '\\' . $regs['endpoint']; } } diff --git a/vhs/services/ServiceRegistry.php b/packages/backend-php/vhs/services/ServiceRegistry.php similarity index 67% rename from vhs/services/ServiceRegistry.php rename to packages/backend-php/vhs/services/ServiceRegistry.php index 813dfa94b..e6e6403a1 100644 --- a/vhs/services/ServiceRegistry.php +++ b/packages/backend-php/vhs/services/ServiceRegistry.php @@ -12,7 +12,13 @@ use vhs\Logger; use vhs\Singleton; +/** + * @method static \vhs\services\ServiceRegistry getInstance() + * + * @typescript + */ class ServiceRegistry extends Singleton { + /** @var array */ private $services = []; /** @@ -25,10 +31,12 @@ final public static function get($key) { } /** - * @param Logger $logger - * @param string $key - * @param string $namespace - * @param string $path + * @param \vhs\Logger $logger + * @param string $key + * @param string $namespace + * @param string $path + * + * @return void */ final public static function register(Logger &$logger, $key, $namespace, $path) { ServiceRegistry::getInstance()->services[$key] = new ServiceHandler($logger, $namespace, $path, $key . '/'); diff --git a/vhs/services/endpoints/Endpoint.php b/packages/backend-php/vhs/services/endpoints/Endpoint.php similarity index 81% rename from vhs/services/endpoints/Endpoint.php rename to packages/backend-php/vhs/services/endpoints/Endpoint.php index c71219561..e2a5495a6 100644 --- a/vhs/services/endpoints/Endpoint.php +++ b/packages/backend-php/vhs/services/endpoints/Endpoint.php @@ -9,25 +9,39 @@ namespace vhs\services\endpoints; -use vhs\Logger; use vhs\security\CurrentUser; use vhs\security\exceptions\UnauthorizedException; use vhs\services\exceptions\InvalidContractException; use vhs\services\exceptions\InvalidRequestException; use vhs\services\IContract; +/** @typescript */ abstract class Endpoint implements IEndpoint { - /** @var Logger $logger */ + /** @var \vhs\Logger $logger */ public $logger; + /** + * internal_service. + * + * @var mixed + */ protected $internal_service; + /** + * __construct. + * + * @param \vhs\services\IContract $service + * + * @return void + */ protected function __construct(IContract $service) { $this->internal_service = $service; } /** - * @return Endpoint + * getInstance. + * + * @return \vhs\services\endpoints\Endpoint */ final public static function getInstance() { static $aoInstance = []; @@ -41,6 +55,11 @@ final public static function getInstance() { return $aoInstance[$class]; } + /** + * discover. + * + * @return mixed + */ final public function discover() { $contract = $this->getContract(); @@ -59,6 +78,11 @@ final public function discover() { return $this->serializeOutput((object) $out); } + /** + * getAllPermissions. + * + * @return array + */ final public function getAllPermissions() { $contract = $this->getContract(); @@ -71,6 +95,18 @@ final public function getAllPermissions() { return $allPermissions; } + /** + * handleRequest. + * + * @param mixed $method + * @param mixed $data + * + * @throws \vhs\security\exceptions\UnauthorizedException + * @throws \vhs\services\exceptions\InvalidContractException + * @throws \vhs\services\exceptions\InvalidRequestException + * + * @return mixed + */ final public function handleRequest($method, $data) { $args = $this->deserializeInput($data); @@ -141,15 +177,19 @@ final public function handleRequest($method, $data) { } /** - * @return \ReflectionClass + * getContract. * - * @throws \Exception + + * @throws \vhs\services\exceptions\InvalidContractException + * + * @return mixed */ private function getContract() { - //TODO this would be a good place to implement a memcache registry of permissions & service endpoints + // TODO this would be a good place to implement a memcache registry of permissions & service endpoints $serviceClass = new \ReflectionClass($this->internal_service); $contract = null; + foreach ($serviceClass->getInterfaces() as $interface) { if (array_key_exists('vhs\\services\\IContract', $interface->getInterfaces())) { $contract = $interface; @@ -163,6 +203,13 @@ private function getContract() { return $contract; } + /** + * getMethodPermissions. + * + * @param \ReflectionMethod $method + * + * @return string[][] + */ private function getMethodPermissions(\ReflectionMethod $method) { $comments = $method->getDocComment(); @@ -179,6 +226,11 @@ private function getMethodPermissions(\ReflectionMethod $method) { return $permissions; } - private function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { } } diff --git a/packages/backend-php/vhs/services/endpoints/IEndpoint.php b/packages/backend-php/vhs/services/endpoints/IEndpoint.php new file mode 100644 index 000000000..f866f3f4d --- /dev/null +++ b/packages/backend-php/vhs/services/endpoints/IEndpoint.php @@ -0,0 +1,49 @@ + */ public $headers; /** @var string */ @@ -19,6 +20,11 @@ class HttpRequest { /** @var string */ public $url; + /** + * __construct. + * + * @return void + */ public function __construct() { } } diff --git a/vhs/web/HttpRequestHandler.php b/packages/backend-php/vhs/web/HttpRequestHandler.php similarity index 62% rename from vhs/web/HttpRequestHandler.php rename to packages/backend-php/vhs/web/HttpRequestHandler.php index de1dbdc87..1ccd3154b 100644 --- a/vhs/web/HttpRequestHandler.php +++ b/packages/backend-php/vhs/web/HttpRequestHandler.php @@ -9,6 +9,14 @@ namespace vhs\web; +/** @typescript */ abstract class HttpRequestHandler { + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ abstract public function handle(HttpServer $server); } diff --git a/vhs/web/HttpServer.php b/packages/backend-php/vhs/web/HttpServer.php similarity index 62% rename from vhs/web/HttpServer.php rename to packages/backend-php/vhs/web/HttpServer.php index e324039f1..178337d5a 100644 --- a/vhs/web/HttpServer.php +++ b/packages/backend-php/vhs/web/HttpServer.php @@ -11,20 +11,77 @@ use vhs\Logger; use vhs\loggers\SilentLogger; +use vhs\services\exceptions\InvalidRequestException; +use vhs\web\enums\HttpStatusCodes; use vhs\web\modules\HttpServerInfoModule; +/** @typescript */ class HttpServer { - /** @var HttpRequest */ + /** + * logger. + * + * @var \vhs\Logger + */ + public $logger; + + /** + * request. + * + * @var \vhs\web\HttpRequest + */ public $request; + + /** + * endset. + * + * @var bool + */ private $endset = false; + + /** + * handling. + * + * @var bool + */ private $handling = false; + + /** + * headerBuffer. + * + * @var callable[] + */ private $headerBuffer = []; + + /** + * http_response_code. + * + * @var mixed + */ private $http_response_code; - private $logger; + + /** + * modules. + * + * @var \vhs\web\IHttpModule[] + */ private $modules = []; + + /** + * outputBuffer. + * + * @var callable[] + */ private $outputBuffer = []; - public function __construct(HttpServerInfoModule $infoModule = null, Logger &$logger = null) { + /** + * __construct. + * + * @param \vhs\web\modules\HttpServerInfoModule $infoModule + * @param \vhs\Logger $logger + * + * @return void + */ + public function __construct(?HttpServerInfoModule $infoModule = null, ?Logger &$logger = null) { if (is_null($logger)) { $this->logger = new SilentLogger(); } else { @@ -38,6 +95,11 @@ public function __construct(HttpServerInfoModule $infoModule = null, Logger &$lo } } + /** + * clear. + * + * @return void + */ public function clear() { if ($this->endset) { return; @@ -47,9 +109,16 @@ public function clear() { $this->headerBuffer = []; unset($this->outputBuffer); $this->outputBuffer = []; - $this->http_response_code = 200; + $this->http_response_code = HttpStatusCodes::Success_Ok->value; } + /** + * code. + * + * @param mixed $code + * + * @return void + */ public function code($code) { if ($this->endset) { return; @@ -58,11 +127,22 @@ public function code($code) { $this->http_response_code = $code; } + /** + * end. + * + * @return void + */ public function end() { + $this->logger->debug(__FILE__, __LINE__, __METHOD__, 'trying end'); + if ($this->endset) { + $this->logger->debug(__FILE__, __LINE__, __METHOD__, 'already ended - bailing'); + return; } + $this->logger->debug(__FILE__, __LINE__, __METHOD__, 'setting end'); + $this->endset = true; $self = $this; array_push($this->outputBuffer, function () use ($self) { @@ -70,6 +150,11 @@ public function end() { }); } + /** + * handle. + * + * @return void + */ public function handle() { $this->handling = true; $this->clear(); @@ -80,9 +165,12 @@ public function handle() { session_start(); $exception = null; - /** @var IHttpModule $module */ $index = 0; + + /** @var IHttpModule $module */ foreach ($this->modules as $module) { + $this->logger->debug(__FILE__, __LINE__, __METHOD__, sprintf('trying module: %s', get_class($module))); + if ($this->endset) { break; } @@ -91,11 +179,16 @@ public function handle() { $module->handle($this); } catch (\Exception $ex) { $exception = $ex; + break; } $index += 1; } + if (!$this->endset && is_null($exception)) { + $exception = new InvalidRequestException('No valid service endpoint found', HttpStatusCodes::Server_Error_Service_Unavailable); + } + $this->log($this->request->method . ' ' . $this->request->url . ' ' . json_encode($this->request->headers)); if (!is_null($exception)) { @@ -120,24 +213,47 @@ public function handle() { $this->endResponse(); } - public function header($string, $replace = true, $http_response_code = null) { + /** + * header. + * + * @param mixed $string + * @param mixed $replace + * @param mixed $http_response_code + * + * @return void + */ + public function header($string, $replace = true, $http_response_code = 0) { if ($this->endset) { return; } $self = $this; - array_push($this->headerBuffer, function () use ($self, $string, $replace, $http_response_code) { + array_push($this->headerBuffer, function () use ($string, $replace, $http_response_code) { if (headers_sent() === false) { header($string, $replace, $http_response_code); } }); } + /** + * log. + * + * @param mixed $message + * + * @return void + */ public function log($message) { $this->logger->log($message); } + /** + * output. + * + * @param mixed $data + * + * @return void + */ public function output($data) { if ($this->endset) { return; @@ -148,6 +264,14 @@ public function output($data) { }); } + /** + * redirect. + * + * @param string $url + * @param bool $permanent + * + * @return void + */ public function redirect($url, $permanent = false) { if ($this->endset) { return; @@ -156,35 +280,55 @@ public function redirect($url, $permanent = false) { $this->header('Location: ' . $url, true, $permanent ? 301 : 302); } + /** + * register. + * + * @param \vhs\web\IHttpModule $module + * + * @return void + */ public function register(IHttpModule $module) { if ($this->handling) { $this->log('Failed to register module ' . get_class($module)); + throw new \Exception('Registrations must occur prior to handling a request'); } array_push($this->modules, $module); } + /** + * sendOnlyHeaders. + * + * @return void + */ public function sendOnlyHeaders() { if ($this->endset) { return; } $self = $this; - array_push($this->headerBuffer, function () use ($self) { + array_push($this->headerBuffer, function (): never { exit(); }); } + /** + * endResponse. + * + * @return void + */ private function endResponse() { $exception = null; - /** @var IHttpModule $module */ $index = 0; + + /** @var IHttpModule $module */ foreach ($this->modules as $module) { try { $module->endResponse($this); } catch (\Exception $ex) { $exception = $ex; + break; } $index += 1; diff --git a/vhs/web/HttpUtil.php b/packages/backend-php/vhs/web/HttpUtil.php similarity index 77% rename from vhs/web/HttpUtil.php rename to packages/backend-php/vhs/web/HttpUtil.php index 3f7811152..da4e85442 100644 --- a/vhs/web/HttpUtil.php +++ b/packages/backend-php/vhs/web/HttpUtil.php @@ -9,9 +9,10 @@ namespace vhs\web; +/** @typescript */ class HttpUtil { /** - * @returns HttpRequest + * @return \vhs\web\HttpRequest */ public static function getCurrentRequest() { $req = new HttpRequest(); @@ -23,16 +24,32 @@ public static function getCurrentRequest() { return $req; } + /** + * getRequestMethod. + * + * @return string + */ public static function getRequestMethod() { return $_SERVER['REQUEST_METHOD']; } + /** + * getRequestUrl. + * + * @return string + */ public static function getRequestUrl() { return $_SERVER['SCRIPT_NAME']; } + /** + * parseRequestHeaders. + * + * @return array + */ public static function parseRequestHeaders() { $headers = []; + foreach ($_SERVER as $key => $value) { if (substr($key, 0, 5) != 'HTTP_') { continue; @@ -40,6 +57,7 @@ public static function parseRequestHeaders() { $header = str_replace(' ', '-', ucwords(str_replace('_', ' ', strtolower(substr($key, 5))))); $headers[$header] = $value; } + return $headers; } } diff --git a/packages/backend-php/vhs/web/IHttpModule.php b/packages/backend-php/vhs/web/IHttpModule.php new file mode 100644 index 000000000..0bc13af29 --- /dev/null +++ b/packages/backend-php/vhs/web/IHttpModule.php @@ -0,0 +1,41 @@ +realm = $realm; $this->authorizer = $authorizer; } + /** + * endResponse. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function endResponse(HttpServer $server) { } + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function handle(HttpServer $server) { if (array_key_exists('PHP_AUTH_USER', $_SERVER) && $_SERVER['PHP_AUTH_USER'] && !$this->authorizer->isAuthenticated()) { try { @@ -43,12 +76,28 @@ public function handle(HttpServer $server) { } } + /** + * handleException. + * + * @param \vhs\web\HttpServer $server + * @param \Exception $ex + * + * @return void + */ public function handleException(HttpServer $server, \Exception $ex) { if (get_class($ex) === 'vhs\\security\\exceptions\\UnauthorizedException') { $this->requestAuth($server, $ex->getMessage()); } } + /** + * requestAuth. + * + * @param \vhs\web\HttpServer $server + * @param mixed $message + * + * @return void + */ private function requestAuth(HttpServer $server, $message) { $server->clear(); $server->header('WWW-Authenticate: Basic realm="' . $this->realm . '"'); diff --git a/vhs/web/modules/HttpBearerTokenAuthModule.php b/packages/backend-php/vhs/web/modules/HttpBearerTokenAuthModule.php similarity index 69% rename from vhs/web/modules/HttpBearerTokenAuthModule.php rename to packages/backend-php/vhs/web/modules/HttpBearerTokenAuthModule.php index 3bfe56255..c22169c9d 100644 --- a/vhs/web/modules/HttpBearerTokenAuthModule.php +++ b/packages/backend-php/vhs/web/modules/HttpBearerTokenAuthModule.php @@ -11,22 +11,53 @@ use vhs\security\BearerTokenCredentials; use vhs\security\IAuthenticate; -use vhs\security\UserPassCredentials; -use vhs\web\HttpBasicCredentials; use vhs\web\HttpServer; use vhs\web\IHttpModule; +/** @typescript */ class HttpBearerTokenAuthModule implements IHttpModule { + /** + * authorizer. + * + * @var mixed + */ private $authorizer; + + /** + * headerKey. + * + * @var string + */ private $headerKey = 'HTTP_AUTHORIZATION'; + /** + * __construct. + * + * @param \vhs\security\IAuthenticate $authorizer + * + * @return void + */ public function __construct(IAuthenticate $authorizer) { $this->authorizer = $authorizer; } + /** + * endResponse. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function endResponse(HttpServer $server) { } + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function handle(HttpServer $server) { $bearerToken = null; @@ -49,6 +80,14 @@ public function handle(HttpServer $server) { } } + /** + * handleException. + * + * @param \vhs\web\HttpServer $server + * @param \Exception $ex + * + * @return void + */ public function handleException(HttpServer $server, \Exception $ex) { } } diff --git a/vhs/web/modules/HttpExceptionHandlerModule.php b/packages/backend-php/vhs/web/modules/HttpExceptionHandlerModule.php similarity index 52% rename from vhs/web/modules/HttpExceptionHandlerModule.php rename to packages/backend-php/vhs/web/modules/HttpExceptionHandlerModule.php index 1ef7ab72d..1b11d8de3 100644 --- a/vhs/web/modules/HttpExceptionHandlerModule.php +++ b/packages/backend-php/vhs/web/modules/HttpExceptionHandlerModule.php @@ -13,26 +13,73 @@ use vhs\web\HttpServer; use vhs\web\IHttpModule; +/** @typescript */ class HttpExceptionHandlerModule implements IHttpModule { //todo do something about the levels here + /** + * level. + * + * @var mixed + * + * @phpstan-ignore property.onlyWritten + */ private $level; - private $logger; + /** + * logger. + * + * @var \vhs\Logger + */ + private $logger = null; + + /** + * __construct. + * + * @param mixed $level + * @param \vhs\Logger $logger + * + * @return void + */ public function __construct($level, Logger &$logger) { $this->level = $level; $this->logger = $logger; } + /** + * endResponse. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function endResponse(HttpServer $server) { } + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function handle(HttpServer $server) { } + /** + * handleException. + * + * @param \vhs\web\HttpServer $server + * @param \Exception $ex + * + * @return void + */ public function handleException(HttpServer $server, \Exception $ex) { $this->logger->log($ex->getMessage()); $this->logger->log($ex->getTraceAsString()); + $server->code($ex->getCode() !== 0 ? $ex->getCode() : 500); + + // @phpstan-ignore if.alwaysFalse if (DEBUG) { $server->output($ex->getMessage()); $server->output($ex->getTraceAsString()); diff --git a/packages/backend-php/vhs/web/modules/HttpJsonServiceHandlerModule.php b/packages/backend-php/vhs/web/modules/HttpJsonServiceHandlerModule.php new file mode 100644 index 000000000..99b54a02f --- /dev/null +++ b/packages/backend-php/vhs/web/modules/HttpJsonServiceHandlerModule.php @@ -0,0 +1,122 @@ +registryKey = $registryKey; + } + + /** + * endResponse. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function endResponse(HttpServer $server) { + } + + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @throws \vhs\services\exceptions\InvalidRequestException + * + * @return void + */ + public function handle(HttpServer $server) { + $input = null; + + $server->header('Content-Type: application/json', true); + + $uri = $server->request->url; + + try { + switch ($server->request->method) { + case 'HEAD': + $server->output(ServiceRegistry::get($this->registryKey)->discover($uri)); + + $server->end(); + + break; + case 'GET': + if (isset($_GET['json'])) { + $input = $_GET['json']; + } else { + $input = json_encode($_GET); + } + + $server->output(ServiceRegistry::get($this->registryKey)->handle($uri, $input)); + + break; + case 'POST': + $server->output(ServiceRegistry::get($this->registryKey)->handle($uri, file_get_contents('php://input'))); + + $server->logger->debug(__FILE__, __LINE__, __METHOD__, 'setting end'); + + break; + //case 'PUT': + //case 'DELETE': + default: + throw new InvalidRequestException(); + // TODO clean up + // break; + } + + $server->logger->debug(__FILE__, __LINE__, __METHOD__, 'setting end'); + $server->end(); + } catch (\Throwable $exception) { + $server->logger->debug( + __FILE__, + __LINE__, + __METHOD__, + sprintf('caught exception: %s(%s)', $exception->getMessage(), $exception->getCode()) + ); + + if ($exception->getCode() !== HttpStatusCodes::Client_Error_Misdirected_Request->value) { + throw $exception; + } + } + } + + /** + * handleException. + * + * @param \vhs\web\HttpServer $server + * @param \Exception $ex + * + * @return void + */ + public function handleException(HttpServer $server, \Exception $ex) { + } +} diff --git a/vhs/web/modules/HttpRequestHandlerModule.php b/packages/backend-php/vhs/web/modules/HttpRequestHandlerModule.php similarity index 58% rename from vhs/web/modules/HttpRequestHandlerModule.php rename to packages/backend-php/vhs/web/modules/HttpRequestHandlerModule.php index fcbe921f4..9b5434da1 100644 --- a/vhs/web/modules/HttpRequestHandlerModule.php +++ b/packages/backend-php/vhs/web/modules/HttpRequestHandlerModule.php @@ -13,15 +13,40 @@ use vhs\web\HttpServer; use vhs\web\IHttpModule; +/** @typescript */ abstract class HttpRequestHandlerModule implements IHttpModule { + /** + * registry. + * + * @var array> + */ private $registry = []; + /** + * __construct. + * + * @return void + */ public function __construct() { } + /** + * endResponse. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function endResponse(HttpServer $server) { } + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function handle(HttpServer $server) { if (array_key_exists($server->request->method, $this->registry)) { if (array_key_exists($server->request->url, $this->registry[$server->request->method])) { @@ -30,9 +55,26 @@ public function handle(HttpServer $server) { } } + /** + * handleException. + * + * @param \vhs\web\HttpServer $server + * @param \Exception $ex + * + * @return void + */ public function handleException(HttpServer $server, \Exception $ex) { } + /** + * register_internal. + * + * @param string $method + * @param string $url + * @param \vhs\web\HttpRequestHandler $handler + * + * @return void + */ protected function register_internal($method, $url, HttpRequestHandler $handler) { if (!array_key_exists($method, $this->registry)) { $this->registry[$method] = []; diff --git a/vhs/web/modules/HttpServerInfoModule.php b/packages/backend-php/vhs/web/modules/HttpServerInfoModule.php similarity index 51% rename from vhs/web/modules/HttpServerInfoModule.php rename to packages/backend-php/vhs/web/modules/HttpServerInfoModule.php index d4c641353..666f8db46 100644 --- a/vhs/web/modules/HttpServerInfoModule.php +++ b/packages/backend-php/vhs/web/modules/HttpServerInfoModule.php @@ -12,20 +12,55 @@ use vhs\web\HttpServer; use vhs\web\IHttpModule; +/** @typescript */ class HttpServerInfoModule implements IHttpModule { + /** + * name. + * + * @var string + */ private $name; + /** + * __construct. + * + * @param string $name + * + * @return void + */ public function __construct($name = 'Madhacks') { $this->name = $name; } + /** + * endResponse. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function endResponse(HttpServer $server) { } + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function handle(HttpServer $server) { $server->header('Server: ' . $this->name); } + /** + * handleException. + * + * @param \vhs\web\HttpServer $server + * @param \Exception $ex + * + * @return void + */ public function handleException(HttpServer $server, \Exception $ex) { } } diff --git a/packages/frontend-react/.bowerrc b/packages/frontend-react/.bowerrc new file mode 100644 index 000000000..5abcb4952 --- /dev/null +++ b/packages/frontend-react/.bowerrc @@ -0,0 +1,5 @@ +{ + "directory": "./public/assets/", + "save": true, + "save-exact": true +} diff --git a/packages/frontend-react/.editorconfig b/packages/frontend-react/.editorconfig new file mode 100644 index 000000000..8ae4323a9 --- /dev/null +++ b/packages/frontend-react/.editorconfig @@ -0,0 +1,9 @@ +[*] +indent_style=space +indent_size=4 +end_of_line=lf +charset=utf-8 +trim_trailing_whitespace=true +insert_final_newline=true +max_line_width=120 +print_width=120 diff --git a/packages/frontend-react/.prettierignore b/packages/frontend-react/.prettierignore new file mode 100644 index 000000000..d23df3420 --- /dev/null +++ b/packages/frontend-react/.prettierignore @@ -0,0 +1,5 @@ +pnpm-lock.yaml +src/routeTree.gen.ts +*.svg +public/assets/ +src/types/nomos.d.ts diff --git a/packages/frontend-react/.storybook/main.ts b/packages/frontend-react/.storybook/main.ts new file mode 100644 index 000000000..d6a5ef115 --- /dev/null +++ b/packages/frontend-react/.storybook/main.ts @@ -0,0 +1,52 @@ +import { mergeConfig } from 'vite' +import tsconfigPaths from 'vite-tsconfig-paths' + +import type { StorybookConfig } from '@storybook/react-vite' + +const includeDirs = [ + '01-atoms', + '02-molecules', + '03-particles', + '04-composites', + '05-materials', + '06-layouts', + '07-pages', + '08-app', + '09-providers' +] + +const stories = [ + '../src/**/*.mdx', + ...includeDirs.map((includeDir) => `../src/components/${includeDir}/**/*.stories.@(js|jsx|mjs|ts|tsx)`) +] + +const config: StorybookConfig = { + stories, + + addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], + + framework: { + name: '@storybook/react-vite', + options: {} + }, + + docs: {}, + + core: { + disableTelemetry: true + }, + + staticDirs: ['../public'], + + typescript: { + reactDocgen: 'react-docgen-typescript' + }, + + viteFinal(config) { + return mergeConfig(config, { + plugins: [tsconfigPaths()] + }) + } +} + +export default config diff --git a/packages/frontend-react/.storybook/preview-head.html b/packages/frontend-react/.storybook/preview-head.html new file mode 100644 index 000000000..2882e410c --- /dev/null +++ b/packages/frontend-react/.storybook/preview-head.html @@ -0,0 +1,4 @@ + + + + diff --git a/packages/frontend-react/.storybook/preview.tsx b/packages/frontend-react/.storybook/preview.tsx new file mode 100644 index 000000000..92af9bae3 --- /dev/null +++ b/packages/frontend-react/.storybook/preview.tsx @@ -0,0 +1,40 @@ +import React from 'react' + +import { RouterProvider, createMemoryHistory, createRootRoute, createRouter } from '@tanstack/react-router' +import { initialize, mswLoader } from 'msw-storybook-addon' + +import type { Preview } from '@storybook/react' + +import '../src/main.css' + +initialize({ + serviceWorker: { url: '/apiMockServiceWorker.js' } +}) + +const preview: Preview = { + loaders: [mswLoader], + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/ + } + } + }, + decorators: [ + (Story) => { + return ( + + ) + } + ] +} + +export default preview diff --git a/packages/frontend-react/README.md b/packages/frontend-react/README.md new file mode 100644 index 000000000..8f372f624 --- /dev/null +++ b/packages/frontend-react/README.md @@ -0,0 +1,20 @@ +# NOMOS React Frontend + +This project aims to provide a replacement frontend for the [NOMOS membership operations system](https://github.com/vhs/nomos). + +## Development + +Amongst others, this project uses the following technologies: + +- FontAwesome +- HeroIcons +- React +- React Toastify +- SWR +- Tailwind +- Tanstack (React) Router +- Typescript +- Vite +- Zod + +For more information, see the [Development Guide](./docs/Development.md) diff --git a/packages/frontend-react/bower.json b/packages/frontend-react/bower.json new file mode 100644 index 000000000..15f4577c1 --- /dev/null +++ b/packages/frontend-react/bower.json @@ -0,0 +1,14 @@ +{ + "name": "@vhs/nomos-frontend-react", + "description": "NOMOS React frontend", + "main": "index.html", + "authors": ["Ty Eggen "], + "license": "GPLv2", + "homepage": "https://github.com/vhs/nomos", + "private": true, + "ignore": ["**/.*", "node_modules", "bower_components", "./public/assets/", "test", "tests"], + "dependencies": { + "fontawesome": "6.7.2", + "pace": "https://github.com/HubSpot/pace.git#1.2.4" + } +} diff --git a/packages/frontend-react/conf/nginx-react-docker-compose.conf b/packages/frontend-react/conf/nginx-react-docker-compose.conf new file mode 100644 index 000000000..6116ca2b5 --- /dev/null +++ b/packages/frontend-react/conf/nginx-react-docker-compose.conf @@ -0,0 +1,31 @@ +server { + listen 80 default_server; + server_name _; + root /var/www/html; + error_page 404 =200 /index.html; + + location ~ (/services/|\.php$) { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_read_timeout 300; + fastcgi_pass backend-php:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME /var/www/html/app/app.php; # ?service=$fastcgi_script_name; + fastcgi_buffers 16 16k; + fastcgi_buffer_size 32k; + include fastcgi_params; + } + + location / { + index index.html; + try_files $uri $uri/ /index.html; + } + + location = /robots.txt { + return 200 "User-agent: *\nDisallow: /"; + } + + location ~* \.php$ { + include fastcgi_params; + fastcgi_pass backend-php:9000; + } +} diff --git a/packages/frontend-react/docs/Development.md b/packages/frontend-react/docs/Development.md new file mode 100644 index 000000000..5a456e130 --- /dev/null +++ b/packages/frontend-react/docs/Development.md @@ -0,0 +1,84 @@ +# Development + +## Source Code Organization + +The sources are organized in the following manner: + +- `src/components` is where all components live +- `src/lib` is where all other non-component code lives +- `src/routes` is where all routing files are kept +- `src/stories` is where all common Storybook files are kept +- `src/styles` is where all common styles files are kept +- `src/types` is where all supportive Typescript typing files reside + +Additionally, the following files are relevant: + +- `index.html` is the primary entry point for the application +- `src/main.tsx` is the primary JSX entrypoint +- `src/main.css` is the main CSS definitions file +- `src/router.tsx` provides routing scaffolding + +## Components + +### Primary Components + +This project uses a variant of the "Atomic React" pattern to organize its components. In short, Atomic React breaks up components into different types(/folders) based on re-use and complexity.
+I.e. an _atom_ is a component of bare function where as a molecule in increasingly complex, and so on. + +This project divides components based on complexity and the inclusion of other components and their respective complexity levels.
+I.e. a simple component that imports another component or external components will always at least be a Molecule. + +This project uses the following levels to achieve this: + +- _Atoms_ are re-usable, simple HTML components +- _Molecules_ are fundamental building blocks that import at least either an Atom or an external component +- _Particles_ offer more functionality (and import Molecule components) +- _Composites_ are re-usable (template) components (and import Particle components) +- _Materials_ are fully re-usable complex modules/components (and import Composite components) + +**Notes**: + +> - A good way to look at classification is that Atoms may only use plain HTML (and not import other components), and every layer beyond an Atom is determined by the highest layer imported. E.g. a Molecule component will always import at least one (1) Atom or external component, and a Particle component will at least import one (1) Molecule component. +> - External components always count as Atoms. +> - If a component has integrated sub-components, count this as one complexity layer and count from there.
E.g. if a sub-component imports a Molecule, then the main component will be a Composite level component.
(Molecule->Particle->Composite). See the Menu component as an example. +> - Every type of component can be scaffolded by using the `pnpm create:_type_` command.
+> E.g. `pnpm create:atom Logo` will create a template component by the name of `Logo` in `src/components/01-atoms/Logo`. + +### Secondary Components + +In addition to the "basic" components above, this project separates the following types of components: + +- _Layouts_ for general layout components. +- _Integrated Pages_ for common components that will adapt based on props passed. (E.g. pages in this directory can be call from an admin route with an admin prop to show the admin page instead of the user page.) +- _Pages_ divided in logical groupings. + - _Admin_ for admin page components. + - _Common_ for non-authenticated page components. + - _User_ for user page components. +- _App_ for app components. +- _Providers_ for React Content Provider components. +- _Templates_ for `generate-react-cli` template components. + +### Folder Structure + +Furthermore, all components use a numbered naming convention to enforce consistent directory ordering: + +- `src/components/00-components` +- `src/components/01-atoms` +- `src/components/02-molecules` +- `src/components/03-particles` +- `src/components/04-composites` +- `src/components/05-materials` +- `src/components/06-layouts` +- `src/components/07-integrated-pages` +- `src/components/07-pages` +- `src/components/08-app` +- `src/components/09-providers` +- `src/components/99-templates` + +**Note**: + +> While new components may be created as a `component` and moved to the appropriate directory later, but components should never be committed to the `00-components` directory. + +## Routing + +The basic page/route workflow consists of a `route` file (that extends a particular `layout`) which calls a `page` that imports all the relevant components. diff --git a/packages/frontend-react/eslint.config.mjs b/packages/frontend-react/eslint.config.mjs new file mode 100644 index 000000000..5482acf95 --- /dev/null +++ b/packages/frontend-react/eslint.config.mjs @@ -0,0 +1,100 @@ +import pluginRouter from '@tanstack/eslint-plugin-router' +import cjs from '@tyisi/config-eslint/cjs' +import js from '@tyisi/config-eslint/js' +import mjs from '@tyisi/config-eslint/mjs' +import ts from '@tyisi/config-eslint/ts' +import tsx from '@tyisi/config-eslint/tsx' +import pluginReactHooks from 'eslint-plugin-react-hooks' +import pluginReactRefresh from 'eslint-plugin-react-refresh' +import pluginStorybook from 'eslint-plugin-storybook' +import unusedImports from 'eslint-plugin-unused-imports' +import globals from 'globals' + +function hoistConfig(config, withReact) { + withReact ??= false + + config[0].languageOptions.parserOptions = { + ecmaVersion: 'latest', + sourceType: 'module', + projectService: { + allowDefaultProject: ['src/', '.storybook/*.ts', '.storybook/*.tsx'] + }, + // @ts-ignore + tsconfigRootDir: process.cwd(), + ecmaFeatures: { + jsx: withReact + } + } + + if (withReact) config[0].languageOptions.globals = { ...config[0].languageOptions.globals, ...globals.browser } + + config[0].languageOptions.globals.describe = false + config[0].languageOptions.globals.it = false + + config[0].languageOptions.globals.Highcharts = false + + config[0].languageOptions.globals.top = false + + if (withReact) config[0].plugins['react-hooks'] = pluginReactHooks + if (withReact) config[0].plugins['react-refresh'] = pluginReactRefresh + if (withReact) config[0].plugins['@tanstack/router'] = pluginRouter + + if (withReact) config[0].rules['react-refresh/only-export-components'] = ['warn', { allowConstantExport: true }] + + if (withReact) + // @ts-ignore + Object.entries(pluginRouter.configs['flat/recommended'][0].rules).forEach(([k, v]) => (config[0].rules[k] = v)) + if (withReact) + Object.entries(pluginReactHooks.configs.recommended.rules).forEach(([k, v]) => (config[0].rules[k] = v)) + if (withReact) + Object.entries(pluginReactHooks.configs.recommended.rules).forEach(([k, v]) => (config[0].rules[k] = v)) + + delete config[0].rules['@typescript-eslint/no-unnecessary-condition'] + + if (withReact) { + config[0].plugins['storybook'] = pluginStorybook + + pluginStorybook.configs['flat/recommended'].forEach((storybookConfigSlice) => { + const newConfigSlice = {} + + if (storybookConfigSlice.files != null) { + newConfigSlice.files = [] + storybookConfigSlice.files.forEach((e) => newConfigSlice.files.push(e)) + } + + if (storybookConfigSlice.rules != null) { + newConfigSlice.rules = { ...config[0].rules } + Object.entries(storybookConfigSlice.rules).forEach(([k, v]) => (newConfigSlice.rules[k] = v)) + } + + config.push({ ...config[0], ...newConfigSlice }) + }) + } + + config[0].plugins['unused-imports'] = unusedImports + config[0].rules['unused-imports/no-unused-imports'] = 'warn' + + config[0].rules['max-params'] = 'off' + config[0].rules['@typescript-eslint/max-params'] = ['error', { max: 12 }] + + config[0].rules['@typescript-eslint/triple-slash-reference'] = [ + 'error', + { lib: 'always', path: 'always', types: 'always' } + ] + + if (withReact) + config[0].rules['jsx-a11y/label-has-associated-control'] = [ + 'error', + { + controlComponents: ['FormControl'], + depth: 3 + } + ] + + config.unshift({ ignores: ['src/routeTree.gen.ts', 'src/types/nomos.d.ts'] }) +} + +hoistConfig(ts, false) +hoistConfig(tsx, true) + +export default [...cjs, ...mjs, ...js, ...ts, ...tsx] diff --git a/packages/frontend-react/generate-react-cli.json b/packages/frontend-react/generate-react-cli.json new file mode 100644 index 000000000..daef5383a --- /dev/null +++ b/packages/frontend-react/generate-react-cli.json @@ -0,0 +1,280 @@ +{ + "usesTypeScript": true, + "usesStyledComponents": false, + "usesCssModule": true, + "cssPreprocessor": "scss", + "testLibrary": "Testing Library", + "component": { + "default": { + "path": "src/components/00-components", + "withLazy": false, + "withStory": true, + "withStyle": false, + "withTest": false, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "atom": { + "path": "src/components/01-atoms", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": false, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "molecule": { + "path": "src/components/02-molecules", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "particle": { + "path": "src/components/03-particles", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "composite": { + "path": "src/components/04-composites", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "material": { + "path": "src/components/05-materials", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "layout": { + "path": "src/components/06-layouts", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "page": { + "path": "src/components/07-pages", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "page:admin": { + "path": "src/components/07-pages/admin", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customDirectory": "AdminTemplateName", + "customTemplates": { + "component": "src/components/99-templates/admin-page/AdminTemplateName.tsx", + "lazy": "src/components/99-templates/admin-page/AdminTemplateName.lazy.tsx", + "story": "src/components/99-templates/admin-page/AdminTemplateName.stories.tsx", + "style": "src/components/99-templates/admin-page/AdminTemplateName.module.css", + "test": "src/components/99-templates/admin-page/AdminTemplateName.test.tsx", + "types": "src/components/99-templates/admin-page/AdminTemplateName.types.ts", + "utils": "src/components/99-templates/admin-page/AdminTemplateName.utils.ts" + } + }, + "page:admin:item": { + "path": "src/components/07-pages/admin", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": false, + "withTypes": true, + "withUtils": true, + "customDirectory": "AdminTemplateName/item", + "customTemplates": { + "component": "src/components/99-templates/admin-page/item/AdminTemplateNameItem.tsx", + "lazy": "src/components/99-templates/admin-page/item/AdminTemplateNameItem.lazy.tsx", + "story": "src/components/99-templates/admin-page/item/AdminTemplateNameItem.stories.tsx", + "test": "src/components/99-templates/admin-page/item/AdminTemplateNameItem.test.ts", + "types": "src/components/99-templates/admin-page/item/AdminTemplateNameItem.types.ts", + "utils": "src/components/99-templates/admin-page/item/AdminTemplateNameItem.utils.ts" + } + }, + "page:common": { + "path": "src/components/07-pages/common", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/page/TemplateName.tsx", + "lazy": "src/components/99-templates/page/TemplateName.lazy.tsx", + "story": "src/components/99-templates/page/TemplateName.stories.tsx", + "style": "src/components/99-templates/page/TemplateName.module.css", + "test": "src/components/99-templates/page/TemplateName.test.tsx", + "types": "src/components/99-templates/page/TemplateName.types.ts", + "utils": "src/components/99-templates/page/TemplateName.utils.ts" + } + }, + "page:public": { + "path": "src/components/07-pages/public", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/page/TemplateName.tsx", + "lazy": "src/components/99-templates/page/TemplateName.lazy.tsx", + "story": "src/components/99-templates/page/TemplateName.stories.tsx", + "style": "src/components/99-templates/page/TemplateName.module.css", + "test": "src/components/99-templates/page/TemplateName.test.tsx", + "types": "src/components/99-templates/page/TemplateName.types.ts", + "utils": "src/components/99-templates/page/TemplateName.utils.ts" + } + }, + "page:user": { + "path": "src/components/07-pages/user", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/page/TemplateName.tsx", + "lazy": "src/components/99-templates/page/TemplateName.lazy.tsx", + "story": "src/components/99-templates/page/TemplateName.stories.tsx", + "style": "src/components/99-templates/page/TemplateName.module.css", + "test": "src/components/99-templates/page/TemplateName.test.tsx", + "types": "src/components/99-templates/page/TemplateName.types.ts", + "utils": "src/components/99-templates/page/TemplateName.utils.ts" + } + }, + "app": { + "path": "src/components/08-app", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "provider": { + "path": "src/components/09-providers", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + } + } +} diff --git a/packages/frontend-react/index.html b/packages/frontend-react/index.html new file mode 100644 index 000000000..5d779e67b --- /dev/null +++ b/packages/frontend-react/index.html @@ -0,0 +1,20 @@ + + + + + + + + NOMOS + + + + + + + + +
+ + + diff --git a/packages/frontend-react/package.json b/packages/frontend-react/package.json new file mode 100644 index 000000000..0fb09e727 --- /dev/null +++ b/packages/frontend-react/package.json @@ -0,0 +1,216 @@ +{ + "dependencies": { + "@heroicons/react": "2.2.0", + "@hookform/resolvers": "^4.1.0", + "@tanstack/react-router": "1.100.0", + "@vitejs/plugin-react": "^4.3.4", + "anderm-react-timeago": "4.4.2", + "chart.js": "4.4.7", + "clsx": "2.1.1", + "crypto-js": "4.2.0", + "javascript-color-gradient": "2.5.0", + "just-install": "2.0.2", + "moment": "2.30.1", + "react": "18.3.1", + "react-chartjs-2": "5.3.0", + "react-dom": "18.3.1", + "react-gravatar": "2.6.3", + "react-hook-form": "^7.54.2", + "react-spinners": "0.15.0", + "react-toastify": "11.0.3", + "swr": "2.3.2", + "wireit": "0.14.12", + "zod": "3.24.1" + }, + "devDependencies": { + "@storybook/addon-docs": "^8.5.5", + "@storybook/addon-essentials": "^8.5.5", + "@storybook/addon-interactions": "^8.5.5", + "@storybook/addon-links": "^8.5.5", + "@storybook/blocks": "^8.5.5", + "@storybook/csf": "^0.1.13", + "@storybook/react": "^8.5.5", + "@storybook/react-vite": "^8.5.5", + "@storybook/test": "^8.5.5", + "@tanstack/eslint-plugin-router": "1.99.3", + "@tanstack/router-cli": "^1.136.6", + "@tanstack/router-devtools": "1.100.0", + "@tanstack/router-plugin": "1.100.0", + "@testing-library/jest-dom": "6.6.3", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", + "@tyisi/config-eslint": "4.0.0", + "@tyisi/config-prettier": "1.0.1", + "@tyisi/config-stylelint": "1.1.0", + "@types/crypto-js": "4.2.2", + "@types/javascript-color-gradient": "2.4.2", + "@types/jest": "29.5.14", + "@types/node": "22.13.1", + "@types/react": "19.0.8", + "@types/react-dom": "19.0.3", + "@types/react-gravatar": "2.6.14", + "@vitejs/plugin-basic-ssl": "1.2.0", + "@vitejs/plugin-react-swc": "3.7.2", + "autoprefixer": "^10.4.20", + "bower": "^1.8.14", + "eslint": "9.20.0", + "eslint-plugin-jest": "28.9.0", + "eslint-plugin-react-hooks": "5.1.0", + "eslint-plugin-react-refresh": "0.4.18", + "eslint-plugin-storybook": "0.11.2", + "eslint-plugin-unused-imports": "^4.1.4", + "generate-react-cli": "8.4.9", + "globals": "15.14.0", + "husky": "9.1.7", + "jest": "29.7.0", + "msw": "2.7.0", + "msw-storybook-addon": "2.0.4", + "postcss": "8.5.1", + "postcss-import": "^16.1.0", + "prettier": "3.4.2", + "prettier-plugin-tailwindcss": "^0.6.11", + "sass": "1.84.0", + "storybook": "^8.5.5", + "stylelint": "16.14.1", + "tailwindcss": "3.4.17", + "typescript": "5.7.3", + "vite": "6.1.0", + "vite-tsconfig-paths": "5.1.4" + }, + "msw": { + "workerDirectory": [ + "public" + ] + }, + "name": "@vhs/nomos-frontend-react", + "private": true, + "scripts": { + "build": "wireit", + "build:storybook": "wireit", + "clean:dist": "wireit", + "create:atom": "generate-react c --type=atom", + "create:component": "generate-react c --type=component", + "create:composite": "generate-react c --type=composite", + "create:layout": "generate-react c --type=layout", + "create:material": "generate-react c --type=material", + "create:molecule": "generate-react c --type=molecule", + "create:page": "generate-react c --type=page", + "create:page:admin": "generate-react c --type=page:admin", + "create:page:admin:item": "generate-react c --type=page:admin:item", + "create:page:common": "generate-react c --type=page:common", + "create:page:public": "generate-react c --type=page:public", + "create:page:user": "generate-react c --type=page:user", + "create:particle": "generate-react c --type=particle", + "create:provider": "generate-react c --type=provider", + "dev": "vite", + "dev:lan": "vite --host", + "generate:routes": "tsr generate", + "fix:storybook:titles": "wireit", + "generate:fontawesome": "wireit", + "generate:fontawesome:names": "wireit", + "generate:fontawesome:types": "wireit", + "generate:validator:implementations": "wireit", + "install:dependencies": "wireit", + "lint": "wireit", + "prepare": "wireit", + "preview": "vite preview", + "storybook": "wireit", + "type-check": "wireit" + }, + "type": "module", + "version": "0.0.0", + "wireit": { + "build": { + "command": "vite build", + "dependencies": [ + "install:dependencies", + "generate:fontawesome", + "compile", + "clean:build:assets" + ] + }, + "build:storybook": { + "command": "storybook build" + }, + "clean:bower:assets": { + "command": "rm -fR ./public/assets/*" + }, + "clean:dist": { + "command": "rm -fR dist/*" + }, + "clean:build:assets": { + "command": "tools/clean-build-assets.sh", + "dependencies": [ + "install:dependencies", + "compile", + "generate:fontawesome" + ] + }, + "compile": { + "command": "tsc", + "dependencies": [ + "install:dependencies", + "generate:fontawesome" + ] + }, + "fix:storybook:titles": { + "command": "./tools/fix-storybook-titles.sh" + }, + "generate:fontawesome": { + "dependencies": [ + "install:dependencies", + "generate:fontawesome:names", + "generate:fontawesome:types" + ] + }, + "generate:fontawesome:names": { + "command": "./tools/generate-fa-names.sh", + "dependencies": [ + "install:dependencies" + ] + }, + "generate:fontawesome:types": { + "command": "./tools/generate-fa-types.sh", + "dependencies": [ + "install:dependencies" + ] + }, + "generate:validator:implementations": { + "command": "./tools/generate-validator-implementations.sh" + }, + "install:bower": { + "command": "bower install --force", + "files": [ + "public/assets/**" + ], + "clean": true + }, + "install:config": { + "command": "./tools/bootstrap-config.sh" + }, + "install:dependencies": { + "dependencies": [ + "install:bower", + "install:config" + ] + }, + "lint": { + "command": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + }, + "prepare": { + "dependencies": [ + "install:dependencies" + ] + }, + "storybook": { + "command": "storybook dev -p 6006 --https --ssl-ca ./certs/ca.crt --ssl-key ./certs/localhost.key --ssl-cert ./certs/localhost.crt", + "dependencies": [ + "fix:storybook:titles" + ], + "service": true + }, + "type-check": { + "command": "tsc --noEmit" + } + } +} diff --git a/packages/frontend-react/postcss.config.js b/packages/frontend-react/postcss.config.js new file mode 100644 index 000000000..801a9de0c --- /dev/null +++ b/packages/frontend-react/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + 'postcss-import': {}, + 'tailwindcss': {}, + 'autoprefixer': {} + } +} diff --git a/packages/frontend-react/prettier.config.mjs b/packages/frontend-react/prettier.config.mjs new file mode 100644 index 000000000..15e3ea33a --- /dev/null +++ b/packages/frontend-react/prettier.config.mjs @@ -0,0 +1,5 @@ +import config from '../../prettier.config.mjs' + +config.printWidth = 120 + +export default config diff --git a/packages/frontend-react/public/apiMockServiceWorker.js b/packages/frontend-react/public/apiMockServiceWorker.js new file mode 100644 index 000000000..9b25f6dab --- /dev/null +++ b/packages/frontend-react/public/apiMockServiceWorker.js @@ -0,0 +1,12 @@ +// eslint-disable-next-line no-undef +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url) + + if (!url.pathname.startsWith('/services/')) { + // Do not propagate this event to other listeners (from MSW) + event.stopImmediatePropagation() + } +}) + +// eslint-disable-next-line no-undef +importScripts('./mockServiceWorker.js') diff --git a/web/badges/cert_cnc_mill_lathe.svg b/packages/frontend-react/public/badges/cert_cnc_mill_lathe.svg similarity index 100% rename from web/badges/cert_cnc_mill_lathe.svg rename to packages/frontend-react/public/badges/cert_cnc_mill_lathe.svg diff --git a/web/badges/cert_laser.svg b/packages/frontend-react/public/badges/cert_laser.svg similarity index 100% rename from web/badges/cert_laser.svg rename to packages/frontend-react/public/badges/cert_laser.svg diff --git a/web/badges/door_access.svg b/packages/frontend-react/public/badges/door_access.svg similarity index 100% rename from web/badges/door_access.svg rename to packages/frontend-react/public/badges/door_access.svg diff --git a/web/badges/key_holder.svg b/packages/frontend-react/public/badges/key_holder.svg similarity index 100% rename from web/badges/key_holder.svg rename to packages/frontend-react/public/badges/key_holder.svg diff --git a/web/badges/key_holder_inactive.svg b/packages/frontend-react/public/badges/key_holder_inactive.svg similarity index 100% rename from web/badges/key_holder_inactive.svg rename to packages/frontend-react/public/badges/key_holder_inactive.svg diff --git a/web/badges/laser.png b/packages/frontend-react/public/badges/laser.png similarity index 100% rename from web/badges/laser.png rename to packages/frontend-react/public/badges/laser.png diff --git a/web/badges/newsletter.svg b/packages/frontend-react/public/badges/newsletter.svg similarity index 100% rename from web/badges/newsletter.svg rename to packages/frontend-react/public/badges/newsletter.svg diff --git a/packages/frontend-react/public/custom/pace.local.css b/packages/frontend-react/public/custom/pace.local.css new file mode 100644 index 000000000..4102bb03f --- /dev/null +++ b/packages/frontend-react/public/custom/pace.local.css @@ -0,0 +1,22 @@ +.pace { + -webkit-pointer-events: none; + pointer-events: none; + + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.pace-inactive { + display: none; +} + +.pace .pace-progress { + background: #2563eb; + position: fixed; + z-index: 2000; + top: 0; + right: 100%; + width: 100%; + height: 2px; +} diff --git a/web/favicon.ico b/packages/frontend-react/public/favicon.ico similarity index 100% rename from web/favicon.ico rename to packages/frontend-react/public/favicon.ico diff --git a/web/images/logo.png b/packages/frontend-react/public/images/logo.png similarity index 100% rename from web/images/logo.png rename to packages/frontend-react/public/images/logo.png diff --git a/packages/frontend-react/public/images/logo.svg b/packages/frontend-react/public/images/logo.svg new file mode 100644 index 000000000..f9957300a --- /dev/null +++ b/packages/frontend-react/public/images/logo.svg @@ -0,0 +1,43 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/web/images/provider/github.png b/packages/frontend-react/public/images/provider/github.png similarity index 100% rename from web/images/provider/github.png rename to packages/frontend-react/public/images/provider/github.png diff --git a/web/images/provider/google.png b/packages/frontend-react/public/images/provider/google.png similarity index 100% rename from web/images/provider/google.png rename to packages/frontend-react/public/images/provider/google.png diff --git a/web/images/provider/pin.png b/packages/frontend-react/public/images/provider/pin.png similarity index 100% rename from web/images/provider/pin.png rename to packages/frontend-react/public/images/provider/pin.png diff --git a/web/images/provider/rfid.png b/packages/frontend-react/public/images/provider/rfid.png similarity index 100% rename from web/images/provider/rfid.png rename to packages/frontend-react/public/images/provider/rfid.png diff --git a/web/images/provider/slack.png b/packages/frontend-react/public/images/provider/slack.png similarity index 100% rename from web/images/provider/slack.png rename to packages/frontend-react/public/images/provider/slack.png diff --git a/web/images/slack.png b/packages/frontend-react/public/images/slack.png similarity index 100% rename from web/images/slack.png rename to packages/frontend-react/public/images/slack.png diff --git a/packages/frontend-react/public/images/under-construction90s-90s.gif b/packages/frontend-react/public/images/under-construction90s-90s.gif new file mode 100644 index 0000000000000000000000000000000000000000..ab87cfc56cd4517552494f847b29ec5c3a105a05 GIT binary patch literal 3541 zcmV;`4Jz_SNk%w1VG02X0r3I=00030-2ngp{{R30A^!_bMO0HmK~P09E-(WD0000X z`2+}a;YE=qKv4YgP6~a}o{S=0UF_!GW z8fQ(UEk_6dORz}a_KG`~X4@`Z`NfrcQt#dccGnTgYj&{Mz7O#RPOLZ?V|afNvyB{~ zu%pV4FJnfaIn>|D6$J+k&1f`fa&+6FE~Z+AWfhTM8;vcyGi}ATr{YdAJ4Nl)-hKnu zEu1&;jm9+uN3pfH;pP{jwq7NDGj$BsO`ppydb@@0kQahaEq?sc>+Y8OO<#e$=Z@!L zr?xI0=yg;&|kVR7g1^bx!gpq#o~J=$wasY*|*9X?zJLcVgl}=1e<|fo7WO zsmNxVIkkupadK9-rW|(O`KB0g?wO?-f8Lnn8-vPOXc>q4h-VdxrrGEhj}~cWo+E~& zCZ}Uw>K}BP*1%k+Svdx3QC5<$Qkb6BnJQ7Mj?iibrG6SKjDeD|rYh0EWbBw_e3P;lmJv%Or=oN~b~|B7>C zI%hWWtMlF*G{i$EASJ(R&6#x1P|iFth`%iib<{blJP6OS+6*YeT5p{w)wx-X8`dw6 zoo|RugN^9gY%9$*H$_thH>A(bEp**)U;VV)eB)i--&E=?_Tc`)s$s=pXRUbK$u@2z z;*kGGPB-ITwT$WInD?zYR-8}i`RAogzPaOZ18q9t(WYK`=YqF>`sp=P_VKSJ@Kc}lEGNJLs_lNxlOF<6 zpg?H(k75Uu9RwrTKk@y}XBNa@1{pZMqXlqy9?X`Z8puKRk+6iqJ7EYNwmKG$(1jZm zVGL#XKpJu{h2_Jc2_1z(8$Qs77A#lSs`j-#t5E5#4chHgH-ey*{(RkFZykZ9~@l|`PUS9S?Y&#Oe5{sBS$aj?+RYLqs$nP zM?TU~k7H~d9@)6Z1m+HeH#FoQ6J|#3)v<*@w4)=J)(1om@@$lxFi2FnJliQ#xs!!2qWqzxho2 zVQG^=7!@avIVp9z?wwT%rVM<^&m49$pgCga&aP=rgqo6mM$;#d3`(?kHZ1?2v#h3! zAi5Zdc2c4Aydd)&T2Oy_tC$vD=qn|eK9H?|LnQTPEj238lGZF_EIns4dpS@I4y$Yu zv8hZano~yZbZR~2C{TUs(%MZ>YSe-QMO#Wx^euI9L_HH!p|HS;rgWw&Wk|r#+0o1i z5v4qJ(N}M3O0mLFsa55XQowrDa5fZq`)sRM5enChmTs>3d!ICmde22Z^_J)ssyVhq|+SE^vRVFN`{sz3*LYTp6Wa z`q1~a#r-X4x4Ylg_BXxvm2O=$88rE7_PoSX@Q@IUGY21Ho{y5SSsBbb3%eDjnA7lG zoqIkHD~ZF=tlMx%tWA-g2Ew@#F*;ZrD;Hx2#;KCAh%lTq83Fy_ z#@}Cmyi62dYRJ^!F)59RWQhg2UrbJ|k}Is@&p26fCXRADuiRrOd-+~CUT2oa49FsL z^$ckKr<$26<~EmL%L!$(oZt0ks`~iObI$XVh0A9g(7DQO8T7N61O4SPC$YBuF(?IIRDxST8;Fun^5e`9-CK`MsKN?O_FAlI=9d!cCZ0mZ7C!> z*4fr}gu1P$Z-X1K+Zi{xsbg-d!YAEmz!1CPB<^>+;V<$ANL=c@2YTbXLWI~izy09v zngTrF^X+$43IG5h`2+<400ICkH~>@tegI+s00RGn52DNM52Kv4T8qxzy!(%32}qXa zxrigmw(g_8@{Gpxjc<0&_l_C}3Io0(4LA%Mmo8+I2|YQ7(JE*vv}!q6<@QVI62at) zxQuR`)9f`H?S7Nv@-Mxvz2o=%zW)yxC^$%1Xn2U2sJO`3==ca3DLMI(6tqKGVi|y$ z39{Mg=>aMts%aT&xFUZ!4eMjvuUFKkvLRSvv;- zrS%JEPgcQe0R81dw{W0|DSd!x+lqvb09NBWC zB$hEl&a_q&OT=~CLsn9XK-+<}vUIGO@-`c$x!=1-qNV-C$()a6l}Nmnl2n2hJs zkNt#F&9ju}1gL7VY8`l$j@PddKlJiR76R8JKhu`gIcum}i(%i4oeLuu-Vu8DhUn{8 z1K_|I1{1bm_%MZ~1pojc`2+<400ICkH~>ZflK^4>00RG%kEzS;52Kt4*o!gUy!#Jk z;suuGiALj?w(i@lB23rzA!ql_pSjix3MW6(@OT>qmdrrWf{fCX&E*py&9Z*_5w&+GU6zW)yxC^$%1Xn2SS!ICJr$O!28=ocx;M_H+N ziHWDVDOcw-<0%E&Im%NidU}+qv&xE=X8Kyw3Pc-ZYlIsXoAxWK${XSfY#i!3Oq_At zJgcly+#H+?ea1Wu{A^7poed|3+kO{qS$FqW*$S7`xyYqV PFz(zCUi0p4xc~q=Nb%zc literal 0 HcmV?d00001 diff --git a/web/images/vhs-member-card-2015-2-full.png b/packages/frontend-react/public/images/vhs-member-card-2015-2-full.png similarity index 100% rename from web/images/vhs-member-card-2015-2-full.png rename to packages/frontend-react/public/images/vhs-member-card-2015-2-full.png diff --git a/web/images/vhs-member-card-2015-2-thumb.png b/packages/frontend-react/public/images/vhs-member-card-2015-2-thumb.png similarity index 100% rename from web/images/vhs-member-card-2015-2-thumb.png rename to packages/frontend-react/public/images/vhs-member-card-2015-2-thumb.png diff --git a/web/images/vhs-member-card-2015-full.png b/packages/frontend-react/public/images/vhs-member-card-2015-full.png similarity index 100% rename from web/images/vhs-member-card-2015-full.png rename to packages/frontend-react/public/images/vhs-member-card-2015-full.png diff --git a/web/images/vhs-member-card-2015-thumb.png b/packages/frontend-react/public/images/vhs-member-card-2015-thumb.png similarity index 100% rename from web/images/vhs-member-card-2015-thumb.png rename to packages/frontend-react/public/images/vhs-member-card-2015-thumb.png diff --git a/packages/frontend-react/public/mockServiceWorker.js b/packages/frontend-react/public/mockServiceWorker.js new file mode 100644 index 000000000..193778d76 --- /dev/null +++ b/packages/frontend-react/public/mockServiceWorker.js @@ -0,0 +1,302 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.7.0' +const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window' + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE' + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM + } + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType + } + } + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()) + } + }, + [responseClone.body] + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window' + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter((value) => value !== 'msw/passthrough') + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive + } + }, + [requestBuffer] + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [channel.port2].concat(transferrables.filter(Boolean))) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true + }) + + return mockedResponse +} diff --git a/packages/frontend-react/public/robots.txt b/packages/frontend-react/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/packages/frontend-react/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/packages/frontend-react/skel/config.json b/packages/frontend-react/skel/config.json new file mode 100644 index 000000000..8c1fa02d7 --- /dev/null +++ b/packages/frontend-react/skel/config.json @@ -0,0 +1,3 @@ +{ + "baseApiUri": "" +} diff --git a/packages/frontend-react/src/components/01-atoms/Col/Col.lazy.tsx b/packages/frontend-react/src/components/01-atoms/Col/Col.lazy.tsx new file mode 100644 index 000000000..15bb99fd4 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Col/Col.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { ColProps } from './Col.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyCol = lazy(async () => await import('./Col')) + +const Col = (props: JSX.IntrinsicAttributes & ColProps): JSX.Element => ( + }> + + +) + +export default Col diff --git a/packages/frontend-react/src/components/01-atoms/Col/Col.stories.tsx b/packages/frontend-react/src/components/01-atoms/Col/Col.stories.tsx new file mode 100644 index 000000000..b25e1e1cc --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Col/Col.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import Col from './Col' + +type StoryType = StoryObj + +const meta: Meta = { + component: Col, + title: '01-Atoms/Col', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'Col' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/Col/Col.tsx b/packages/frontend-react/src/components/01-atoms/Col/Col.tsx new file mode 100644 index 000000000..4f4138eb4 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Col/Col.tsx @@ -0,0 +1,34 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { ColBreakPoint, ColProps } from './Col.types' + +const breakpoints: ColBreakPoint[] = ['default', 'xs', 'sm', 'md', 'lg', 'xl'] + +const Col: FC = ({ children, className, ...restProps }) => { + const breakpointClasses = breakpoints + .filter((breakpoint) => restProps[breakpoint] != null) + .map((breakpoint) => { + const basis = + restProps[breakpoint]?.toString() === '12' + ? `${breakpoint}:basis-full` + : `${breakpoint}:basis-${restProps[breakpoint]}/12` + + return basis + }) + + const classNames = [className, ...breakpointClasses, 'col'].map((cn) => cn?.replace(/default:/, '')) + + if (!classNames.join(' ').includes('basis-')) { + classNames.splice(1, 0, 'shrink grow basis-0') + } + + return ( +
+ {children} +
+ ) +} + +export default Col diff --git a/packages/frontend-react/src/components/01-atoms/Col/Col.types.ts b/packages/frontend-react/src/components/01-atoms/Col/Col.types.ts new file mode 100644 index 000000000..563e52085 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Col/Col.types.ts @@ -0,0 +1,11 @@ +import type { MouseEventHandler, ReactNode } from 'react' + +export type ColBreakPoint = 'default' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' + +export type ColBreakPointRecord = Partial> + +export interface ColProps extends ColBreakPointRecord { + children?: ReactNode + className?: string + onClick?: MouseEventHandler +} diff --git a/web/user/home/home.html b/packages/frontend-react/src/components/01-atoms/Col/Col.utils.ts similarity index 100% rename from web/user/home/home.html rename to packages/frontend-react/src/components/01-atoms/Col/Col.utils.ts diff --git a/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.stories.tsx b/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.stories.tsx new file mode 100644 index 000000000..5974a1815 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.stories.tsx @@ -0,0 +1,18 @@ +import type { JSX } from 'react' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook/common' + +import Conditional from './Conditional' + +export default { + title: '01-Atoms/Conditional', + decorators: [CenteredContentStorybookDecorator] +} + +export const Disabled = (): JSX.Element => Disabled + +Disabled.storyName = 'disabled' + +export const Enabled = (): JSX.Element => Enabled + +Enabled.storyName = 'enabled' diff --git a/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.tsx b/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.tsx new file mode 100644 index 000000000..fade25cd9 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.tsx @@ -0,0 +1,12 @@ +import type { FC } from 'react' + +import type { ConditionalProps } from './Conditional.types' + +// eslint-disable-next-line @typescript-eslint/promise-function-async +const Conditional: FC = ({ condition, fallback, children }) => { + if (!condition) return fallback ?? null + + return <>{children} +} + +export default Conditional diff --git a/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.types.ts b/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.types.ts new file mode 100644 index 000000000..38a1a6b14 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.types.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react' + +export interface ConditionalProps { + condition: boolean + fallback?: ReactNode + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.utils.ts b/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/01-atoms/Container/Container.lazy.tsx b/packages/frontend-react/src/components/01-atoms/Container/Container.lazy.tsx new file mode 100644 index 000000000..3565def16 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Container/Container.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { ContainerProps } from './Container.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyContainer = lazy(async () => await import('./Container')) + +const Container = (props: JSX.IntrinsicAttributes & ContainerProps): JSX.Element => ( + }> + + +) + +export default Container diff --git a/packages/frontend-react/src/components/01-atoms/Container/Container.stories.tsx b/packages/frontend-react/src/components/01-atoms/Container/Container.stories.tsx new file mode 100644 index 000000000..ba6ffb945 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Container/Container.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import Container from './Container' + +type StoryType = StoryObj + +const meta: Meta = { + component: Container, + title: '01-Atoms/Container', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'Container' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/Container/Container.tsx b/packages/frontend-react/src/components/01-atoms/Container/Container.tsx new file mode 100644 index 000000000..a49070e74 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Container/Container.tsx @@ -0,0 +1,17 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { ContainerProps } from './Container.types' + +const Container: FC = ({ fluid, className, children }) => { + fluid ??= false + + return ( +
+ {children} +
+ ) +} + +export default Container diff --git a/packages/frontend-react/src/components/01-atoms/Container/Container.types.ts b/packages/frontend-react/src/components/01-atoms/Container/Container.types.ts new file mode 100644 index 000000000..bae650146 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Container/Container.types.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react' + +export interface ContainerProps { + children?: ReactNode + fluid?: boolean + className?: string +} diff --git a/packages/frontend-react/src/components/01-atoms/Container/Container.utils.ts b/packages/frontend-react/src/components/01-atoms/Container/Container.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.lazy.tsx b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.lazy.tsx new file mode 100644 index 000000000..7ca330f16 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FontAwesomeIconProps } from './FontAwesomeIcon.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFontAwesomeIcon = lazy(async () => await import('./FontAwesomeIcon')) + +const FontAwesomeIcon = (props: JSX.IntrinsicAttributes & FontAwesomeIconProps): JSX.Element => ( + }> + + +) + +export default FontAwesomeIcon diff --git a/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.stories.tsx b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.stories.tsx new file mode 100644 index 000000000..ee2280cea --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook/common' + +import FontAwesomeIcon from './FontAwesomeIcon' + +type StoryType = StoryObj + +const meta: Meta = { + component: FontAwesomeIcon, + title: '01-Atoms/FontAwesomeIcon', + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +export const Default: StoryType = { + args: { + category: 'brand', + icon: 'facebook', + className: 'text-blue-500', + size: '5x' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.tsx b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.tsx new file mode 100644 index 000000000..fe113eb52 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.tsx @@ -0,0 +1,47 @@ +import { useMemo, type FC } from 'react' + +import { clsx } from 'clsx' + +import type { FontAwesomeIconProps } from './FontAwesomeIcon.types' + +const FontAwesomeIcon: FC = ({ + category, + className, + effect, + flip, + icon, + inverse, + pullLeft, + pullRight, + rotate, + size, + stack, + ...restProps +}) => { + const iconClassName = useMemo(() => { + const iconOptions = [] + + iconOptions.push(`fa${(category ?? 'solid')[0]}`) + + const iconArray = Array.isArray(icon) ? icon : [icon] + iconArray.forEach((i) => iconOptions.push(`fa-${i}`)) + + if (effect != null) { + const effectArray = Array.isArray(effect) ? effect : [effect] + effectArray.forEach((e) => iconOptions.push(`fa-${e}`)) + } + if (inverse != null) iconOptions.push(`fa-inverse`) + if (pullLeft != null) iconOptions.push(`fa-pull-left`) + if (pullRight != null) iconOptions.push(`fa-pull-right`) + if (flip != null) iconOptions.push(`fa-${flip}`) + if (size != null) iconOptions.push(`fa-${size}`) + if (rotate != null) iconOptions.push(`fa-${rotate}`) + if (stack != null) iconOptions.push(`fa-${stack === true ? 'stack' : stack}`) + + return iconOptions.join(' ') + }, [icon, category, effect, flip, inverse, pullLeft, pullRight, rotate, size, stack]) + + return +} + +export default FontAwesomeIcon diff --git a/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.types.ts b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.types.ts new file mode 100644 index 000000000..a0e65f5c9 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.types.ts @@ -0,0 +1,27 @@ +import type { + FAB, + FAS, + FontAwesomeCategoryOption, + FontAwesomeEffectOption, + FontAwesomeFlipOption, + FontAwesomeRotateOption, + FontAwesomeSizeOption, + FontAwesomeStackOption +} from '@/types/fontawesome' +import type { CastReactElement, SingleOrArray } from '@/types/utils' + +export type IconProp = FAB | FAS | string[] + +export interface FontAwesomeIconProps extends Partial> { + icon: IconProp + category?: FontAwesomeCategoryOption + className?: string + effect?: SingleOrArray + flip?: FontAwesomeFlipOption + inverse?: boolean + pullLeft?: boolean + pullRight?: boolean + rotate?: FontAwesomeRotateOption + size?: FontAwesomeSizeOption + stack?: FontAwesomeStackOption +} diff --git a/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.utils.ts b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.lazy.tsx b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.lazy.tsx new file mode 100644 index 000000000..bb7c0214b --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { NavBarProps } from './NavBar.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyNavBar = lazy(async () => await import('./NavBar')) + +const NavBar = (props: JSX.IntrinsicAttributes & NavBarProps): JSX.Element => ( + }> + + +) + +export default NavBar diff --git a/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.stories.tsx b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.stories.tsx new file mode 100644 index 000000000..32fb214b6 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import NavBar from './NavBar' + +type StoryType = StoryObj + +const meta: Meta = { + component: NavBar, + title: '01-Atoms/NavBar', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'NavBar' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.tsx b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.tsx new file mode 100644 index 000000000..63a7d7b25 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.tsx @@ -0,0 +1,14 @@ +import type { FC } from 'react' + +import type { NavBarProps } from './NavBar.types' + +const NavBar: FC = ({ children }) => ( +
+ {children} +
+) + +export default NavBar diff --git a/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.types.ts b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.types.ts new file mode 100644 index 000000000..9c211c846 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface NavBarProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.utils.ts b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.lazy.tsx b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.lazy.tsx new file mode 100644 index 000000000..da920ba77 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { OverlayProps } from './Overlay.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyOverlay = lazy(async () => await import('./Overlay')) + +const Overlay = (props: JSX.IntrinsicAttributes & OverlayProps): JSX.Element => ( + }> + + +) + +export default Overlay diff --git a/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.stories.tsx b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.stories.tsx new file mode 100644 index 000000000..09a31820b --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import Overlay from './Overlay' + +type StoryType = StoryObj + +const meta: Meta = { + component: Overlay, + title: '01-Atoms/Overlay', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'Overlay' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.tsx b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.tsx new file mode 100644 index 000000000..db339751c --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.tsx @@ -0,0 +1,31 @@ +import { useCallback, useEffect, type FC } from 'react' + +import type { OverlayProps } from './Overlay.types' + +import useOutsideClick from '@/lib/hooks/useClickOutside' + +const Overlay: FC = ({ children, handler }) => { + const clickHandler = useCallback(() => { + if (handler != null) handler(false) + }, [handler]) + + const ref = useOutsideClick(clickHandler) + + useEffect(() => { + ;(document.getElementsByTagName('HTML')[0] as HTMLElement).style.overflow = 'hidden' + + return () => { + ;(document.getElementsByTagName('HTML')[0] as HTMLElement).style.overflow = 'auto' + } + }, []) + + return ( +
+
+ {children} +
+
+ ) +} + +export default Overlay diff --git a/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.types.ts b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.types.ts new file mode 100644 index 000000000..f49660db6 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.types.ts @@ -0,0 +1,8 @@ +import type { ReactNode } from 'react' + +import type { ReactAction } from '@/types/ui' + +export interface OverlayProps { + children?: ReactNode + handler?: ReactAction +} diff --git a/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.utils.ts b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/01-atoms/Pill/Pill.lazy.tsx b/packages/frontend-react/src/components/01-atoms/Pill/Pill.lazy.tsx new file mode 100644 index 000000000..8d557f16e --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Pill/Pill.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { PillProps } from './Pill.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyPill = lazy(async () => await import('./Pill')) + +const Pill = (props: JSX.IntrinsicAttributes & PillProps): JSX.Element => ( + }> + + +) + +export default Pill diff --git a/packages/frontend-react/src/components/01-atoms/Pill/Pill.stories.tsx b/packages/frontend-react/src/components/01-atoms/Pill/Pill.stories.tsx new file mode 100644 index 000000000..9e7d3e27b --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Pill/Pill.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook' + +import Pill from './Pill' + +type StoryType = StoryObj + +const meta: Meta = { + component: Pill, + title: '01-Atoms/Pill', + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'Pill' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/Pill/Pill.tsx b/packages/frontend-react/src/components/01-atoms/Pill/Pill.tsx new file mode 100644 index 000000000..bf823d247 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Pill/Pill.tsx @@ -0,0 +1,19 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { PillProps } from './Pill.types' + +const Pill: FC = ({ children, className }) => ( +
+ {children} +
+) + +export default Pill diff --git a/packages/frontend-react/src/components/01-atoms/Pill/Pill.types.ts b/packages/frontend-react/src/components/01-atoms/Pill/Pill.types.ts new file mode 100644 index 000000000..d93fabaa8 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Pill/Pill.types.ts @@ -0,0 +1,6 @@ +import type { ReactNode } from 'react' + +export interface PillProps { + children?: ReactNode + className?: string +} diff --git a/packages/frontend-react/src/components/01-atoms/Pill/Pill.utils.ts b/packages/frontend-react/src/components/01-atoms/Pill/Pill.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/01-atoms/Popover/Popover.lazy.tsx b/packages/frontend-react/src/components/01-atoms/Popover/Popover.lazy.tsx new file mode 100644 index 000000000..713b83deb --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Popover/Popover.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { PopoverProps } from './Popover.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyPopover = lazy(async () => await import('./Popover')) + +const Popover = (props: JSX.IntrinsicAttributes & PopoverProps): JSX.Element => ( + }> + + +) + +export default Popover diff --git a/packages/frontend-react/src/components/01-atoms/Popover/Popover.stories.tsx b/packages/frontend-react/src/components/01-atoms/Popover/Popover.stories.tsx new file mode 100644 index 000000000..bf5536515 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Popover/Popover.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import Popover from './Popover' + +type StoryType = StoryObj + +const meta: Meta = { + component: Popover, + title: '01-Atoms/Popover', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + content: 'Popover', + popover: 'Popover Content' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/Popover/Popover.tsx b/packages/frontend-react/src/components/01-atoms/Popover/Popover.tsx new file mode 100644 index 000000000..ff3d02c33 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Popover/Popover.tsx @@ -0,0 +1,20 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { PopoverProps } from './Popover.types' + +// TODO make popover a clickable toggle to persist popover for easy interaction + +const Popover: FC = ({ className, content, popover }) => ( +
+
{content}
+
+
+ {popover} +
+
+
+) + +export default Popover diff --git a/packages/frontend-react/src/components/01-atoms/Popover/Popover.types.ts b/packages/frontend-react/src/components/01-atoms/Popover/Popover.types.ts new file mode 100644 index 000000000..458ddd873 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Popover/Popover.types.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react' + +export interface PopoverProps { + className?: string + content: ReactNode + popover: ReactNode +} diff --git a/packages/frontend-react/src/components/01-atoms/Popover/Popover.utils.ts b/packages/frontend-react/src/components/01-atoms/Popover/Popover.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/01-atoms/Row/Row.lazy.tsx b/packages/frontend-react/src/components/01-atoms/Row/Row.lazy.tsx new file mode 100644 index 000000000..0df366f04 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Row/Row.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { RowProps } from './Row.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyRow = lazy(async () => await import('./Row')) + +const Row = (props: JSX.IntrinsicAttributes & RowProps): JSX.Element => ( + }> + + +) + +export default Row diff --git a/packages/frontend-react/src/components/01-atoms/Row/Row.stories.tsx b/packages/frontend-react/src/components/01-atoms/Row/Row.stories.tsx new file mode 100644 index 000000000..459c147db --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Row/Row.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import Row from './Row' + +type StoryType = StoryObj + +const meta: Meta = { + component: Row, + title: '01-Atoms/Row', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'Row' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/Row/Row.tsx b/packages/frontend-react/src/components/01-atoms/Row/Row.tsx new file mode 100644 index 000000000..d0f74e66c --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Row/Row.tsx @@ -0,0 +1,21 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { RowProps } from './Row.types' + +const Row: FC = ({ children, className, noWrap, ...restProps }) => { + noWrap ??= false + + return ( +
+ {children} +
+ ) +} + +export default Row diff --git a/packages/frontend-react/src/components/01-atoms/Row/Row.types.ts b/packages/frontend-react/src/components/01-atoms/Row/Row.types.ts new file mode 100644 index 000000000..de78a270f --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Row/Row.types.ts @@ -0,0 +1,8 @@ +import type { ReactNode } from 'react' + +import type { CastReactElement } from '@/types/utils' + +export interface RowProps extends CastReactElement<'div'> { + children?: ReactNode + noWrap?: boolean +} diff --git a/packages/frontend-react/src/components/01-atoms/Row/Row.utils.ts b/packages/frontend-react/src/components/01-atoms/Row/Row.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.lazy.tsx b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.lazy.tsx new file mode 100644 index 000000000..520cee135 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { TableActionsCellProps } from './TableActionsCell.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyTableActionsCell = lazy(async () => await import('./TableActionsCell')) + +const TableActionsCell = (props: JSX.IntrinsicAttributes & TableActionsCellProps): JSX.Element => ( + }> + + +) + +export default TableActionsCell diff --git a/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.stories.tsx b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.stories.tsx new file mode 100644 index 000000000..9691ef8d4 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import TableActionsCell from './TableActionsCell' + +type StoryType = StoryObj + +const meta: Meta = { + component: TableActionsCell, + title: '01-Atoms/TableActionsCell', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'TableActionsCell' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.tsx b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.tsx new file mode 100644 index 000000000..ccc263d1f --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.tsx @@ -0,0 +1,11 @@ +import type { FC } from 'react' + +import type { TableActionsCellProps } from './TableActionsCell.types' + +const TableActionsCell: FC = ({ className, children }) => ( + +
{children}
+ +) + +export default TableActionsCell diff --git a/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.types.ts b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.types.ts new file mode 100644 index 000000000..9944eb725 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.types.ts @@ -0,0 +1,6 @@ +import type { ReactNode } from 'react' + +export interface TableActionsCellProps { + children?: ReactNode + className?: string +} diff --git a/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.utils.ts b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.lazy.tsx b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.lazy.tsx new file mode 100644 index 000000000..0ab182ddc --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { TablePageRowProps } from './TablePageRow.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyTablePageRow = lazy(async () => await import('./TablePageRow')) + +const TablePageRow = (props: JSX.IntrinsicAttributes & TablePageRowProps): JSX.Element => ( + }> + + +) + +export default TablePageRow diff --git a/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.settings.ts b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.settings.ts new file mode 100644 index 000000000..e91f968d3 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.settings.ts @@ -0,0 +1,17 @@ +export const TablePageRowFieldsClasses: Record = { + 0: 'data-fields-default', + 1: 'data-fields-1', + 2: 'data-fields-2', + 3: 'data-fields-3', + 4: 'data-fields-4', + 5: 'data-fields-5', + 6: 'data-fields-6', + 7: 'data-fields-7', + 8: 'data-fields-8', + 9: 'data-fields-9', + 10: 'data-fields-10', + 11: 'data-fields-11', + 12: 'data-fields-12' +} + +export const TablePageRowDefaultFieldsValue = 0 diff --git a/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.stories.tsx b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.stories.tsx new file mode 100644 index 000000000..b081d6b0a --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import TableDataCell from '@/components/02-molecules/TableDataCell/TableDataCell' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook' + +import TablePageRow from './TablePageRow' + +type StoryType = StoryObj + +const meta: Meta = { + component: TablePageRow, + title: '01-Atoms/TablePageRow', + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +export const Default: StoryType = { + args: { + children: [ + + Cell1 + , + + Cell2 + + ] + } +} diff --git a/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.tsx b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.tsx new file mode 100644 index 000000000..ec731ea05 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.tsx @@ -0,0 +1,21 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { TablePageRowProps } from './TablePageRow.types' + +import { TablePageRowFieldsClasses } from './TablePageRow.settings' + +const TablePageRow: FC = ({ children }) => { + const fields = Array.isArray(children) ? children.length : 0 + + const cssClass = TablePageRowFieldsClasses[fields] ?? 'data-fields-default' + + return ( + + {children} + + ) +} + +export default TablePageRow diff --git a/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.types.ts b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.types.ts new file mode 100644 index 000000000..d2030221d --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface TablePageRowProps { + children: ReactNode +} diff --git a/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.utils.ts b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.lazy.tsx b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.lazy.tsx new file mode 100644 index 000000000..aaa39f121 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { AccountStatusBadgeProps } from './AccountStatusBadge.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyAccountStatusBadge = lazy(async () => await import('./AccountStatusBadge')) + +const AccountStatusBadge = (props: JSX.IntrinsicAttributes & AccountStatusBadgeProps): JSX.Element => ( + }> + + +) + +export default AccountStatusBadge diff --git a/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.module.css b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.module.css new file mode 100644 index 000000000..3cdb5f84f --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.module.css @@ -0,0 +1,11 @@ +.Icon { + @apply inline h-5 w-5; +} + +.Icon > svg { + @apply inline h-5 w-5 rounded-full p-0; +} + +.Icon > svg > * { + @apply bg-white; +} diff --git a/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.stories.tsx b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.stories.tsx new file mode 100644 index 000000000..a98de06d9 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.stories.tsx @@ -0,0 +1,80 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import Col from '@/components/01-atoms/Col/Col' +import Row from '@/components/01-atoms/Row/Row' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook/common' + +import AccountStatusBadge from './AccountStatusBadge' + +type StoryType = StoryObj + +const meta: Meta = { + component: AccountStatusBadge, + title: '02-Molecules/AccountStatusBadge', + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +export const Default: StoryType = { + render: () => ( + <> + + + Active + + + + + Banned + + + + + Inactive + + + + + Pending + + + + + Unknown + + + + ) +} + +export const Active: StoryType = { + args: { + status: 'Active' + } +} + +export const Banned: StoryType = { + args: { + status: 'Banned' + } +} + +export const Inactive: StoryType = { + args: { + status: 'Inactive' + } +} + +export const Pending: StoryType = { + args: { + status: 'Pending' + } +} + +export const Unknown: StoryType = { + args: { + status: 'Unknown' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.tsx b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.tsx new file mode 100644 index 000000000..badb5f145 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.tsx @@ -0,0 +1,45 @@ +import type { FC, JSX } from 'react' + +import { + CheckCircleIcon, + ClockIcon, + NoSymbolIcon, + PauseCircleIcon, + QuestionMarkCircleIcon +} from '@heroicons/react/16/solid' + +import type { AccountStatusBadgeProps } from './AccountStatusBadge.types' + +import styles from './AccountStatusBadge.module.css' + +const BannedIcon = +const PendingIcon = +const ActiveIcon = +const InactiveIcon = +const UnknownIcon = + +const getStatusIcon = (status: string | null): JSX.Element => { + switch (status) { + case 'Active': + return ActiveIcon + case 'Banned': + return BannedIcon + case 'Inactive': + return InactiveIcon + case 'Pending': + return PendingIcon + default: + return UnknownIcon + } +} + +const AccountStatusBadge: FC = ({ status, className }) => { + return ( +
+
{getStatusIcon(status)}
+
 {status}
+
+ ) +} + +export default AccountStatusBadge diff --git a/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.types.ts b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.types.ts new file mode 100644 index 000000000..102f49f61 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.types.ts @@ -0,0 +1,5 @@ +import type { CastReactElement } from '@/types/utils' + +export interface AccountStatusBadgeProps extends CastReactElement<'div'> { + status: string | null +} diff --git a/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.utils.ts b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.lazy.tsx b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.lazy.tsx new file mode 100644 index 000000000..0d6f0239f --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { AdminGuardProps } from './AdminGuard.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyAdminGuard = lazy(async () => await import('./AdminGuard')) + +const AdminGuard = (props: JSX.IntrinsicAttributes & AdminGuardProps): JSX.Element => ( + }> + + +) + +export default AdminGuard diff --git a/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.stories.tsx b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.stories.tsx new file mode 100644 index 000000000..39206927d --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import AdminGuard from './AdminGuard' + +type StoryType = StoryObj + +const meta: Meta = { + component: AdminGuard, + title: '02-Molecules/AdminGuard', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'AdminGuard' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.tsx b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.tsx new file mode 100644 index 000000000..41fb72d7a --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.tsx @@ -0,0 +1,23 @@ +import type { FC } from 'react' + +import { Navigate, useLocation } from '@tanstack/react-router' + +import type { AdminGuardProps } from './AdminGuard.types' + +import useAuth from '@/lib/hooks/useAuth' + +const AdminGuard: FC = ({ children }) => { + const { isAuthenticated, currentUser } = useAuth() + + const pathname = useLocation({ + select: (location) => location.pathname + }) + + if (!isAuthenticated) return + + if (!(currentUser?.hasPermission('administrator') ?? false)) return + + return <>{children} +} + +export default AdminGuard diff --git a/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.types.ts b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.types.ts new file mode 100644 index 000000000..171f45cc9 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface AdminGuardProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.utils.ts b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/Button/Button.lazy.tsx b/packages/frontend-react/src/components/02-molecules/Button/Button.lazy.tsx new file mode 100644 index 000000000..51a108f69 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Button/Button.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { ButtonProps } from './Button.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyButton = lazy(async () => await import('./Button')) + +const Button = (props: JSX.IntrinsicAttributes & ButtonProps): JSX.Element => ( + }> + + +) + +export default Button diff --git a/packages/frontend-react/src/components/02-molecules/Button/Button.stories.tsx b/packages/frontend-react/src/components/02-molecules/Button/Button.stories.tsx new file mode 100644 index 000000000..0bf7fd8bb --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Button/Button.stories.tsx @@ -0,0 +1,86 @@ +import type { ButtonProps } from './Button.types' +import type { Meta, StoryObj } from '@storybook/react' + +import Col from '@/components/01-atoms/Col/Col' +import Row from '@/components/01-atoms/Row/Row' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook/common' + +import Button from './Button' + +type StoryType = StoryObj + +const meta: Meta = { + title: '02-Molecules/Button', + component: Button, + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +export const Default: StoryType = { + render: () => ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export const Primary: StoryType = { args: { variant: 'primary', children: 'Primary' } } +export const Secondary: StoryType = { args: { variant: 'secondary', children: 'Secondary' } } +export const Success: StoryType = { args: { variant: 'success', children: 'Success' } } +export const Warning: StoryType = { args: { variant: 'warning', children: 'Warning' } } +export const Danger: StoryType = { args: { variant: 'danger', children: 'Danger' } } +export const Info: StoryType = { args: { variant: 'info', children: 'Info' } } +export const Light: StoryType = { args: { variant: 'light', children: 'Light' } } +export const Dark: StoryType = { args: { variant: 'dark', children: 'Dark' } } +export const Link: StoryType = { args: { variant: 'link', children: 'Link' } } diff --git a/packages/frontend-react/src/components/02-molecules/Button/Button.styles.ts b/packages/frontend-react/src/components/02-molecules/Button/Button.styles.ts new file mode 100644 index 000000000..a3ac2b319 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Button/Button.styles.ts @@ -0,0 +1,18 @@ +import type { ButtonVariantTypes } from './Button.types' + +const variants: Record = { + primary: 'btn-primary', + secondary: 'btn-secondary', + success: 'btn-success', + warning: 'btn-warning', + danger: 'btn-danger', + info: 'btn-info', + light: 'btn-light', + dark: 'btn-dark', + link: 'btn-link', + none: 'btn-none' +} + +const styles = { variants } + +export default styles diff --git a/packages/frontend-react/src/components/02-molecules/Button/Button.tsx b/packages/frontend-react/src/components/02-molecules/Button/Button.tsx new file mode 100644 index 000000000..c254d6ce4 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Button/Button.tsx @@ -0,0 +1,31 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { ButtonProps } from './Button.types' + +import styles from './Button.styles' + +const Button: FC = ({ children, circle, className, small, variant, ...restProps }) => { + variant ??= 'primary' + small ??= false + circle ??= false + + return ( + + ) +} + +export default Button diff --git a/packages/frontend-react/src/components/02-molecules/Button/Button.types.ts b/packages/frontend-react/src/components/02-molecules/Button/Button.types.ts new file mode 100644 index 000000000..3b8751099 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Button/Button.types.ts @@ -0,0 +1,22 @@ +import type { ReactNode } from 'react' + +import type { CastReactElement } from '@/types/utils' + +export type ButtonVariantTypes = + | 'primary' + | 'secondary' + | 'success' + | 'warning' + | 'danger' + | 'info' + | 'light' + | 'dark' + | 'link' + | 'none' + +export interface ButtonProps extends CastReactElement<'button'> { + children?: ReactNode + circle?: boolean + small?: boolean + variant?: ButtonVariantTypes +} diff --git a/packages/frontend-react/src/components/02-molecules/Button/Button.utils.ts b/packages/frontend-react/src/components/02-molecules/Button/Button.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.lazy.tsx b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.lazy.tsx new file mode 100644 index 000000000..8c156daca --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { ComplexChartProps } from './ComplexChart.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyComplexChart = lazy(async () => await import('./ComplexChart')) + +const ComplexChart = (props: JSX.IntrinsicAttributes & ComplexChartProps): JSX.Element => ( + }> + + +) + +export default ComplexChart diff --git a/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.stories.tsx b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.stories.tsx new file mode 100644 index 000000000..cd91317c3 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.stories.tsx @@ -0,0 +1,34 @@ +import type { JSX } from 'react' + +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import ComplexChart from './ComplexChart' + +type StoryType = StoryObj + +const meta: Meta = { + component: ComplexChart, + title: '02-Molecules/ComplexChart', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + component: (): JSX.Element => <>Test, + displayName: 'Test', + height: 350, + width: 350 + } +} diff --git a/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.tsx b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.tsx new file mode 100644 index 000000000..6035c656a --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.tsx @@ -0,0 +1,24 @@ +import type { FC } from 'react' + +import type { ComplexChartProps } from './ComplexChart.types' + +type DimensionProps = + | Record + | { width: number } + | { height: number } + | { width: number; height: number } + +const ComplexChart: FC = ({ width, height, component: Component, ...chartProps }) => { + const dimensionProps: DimensionProps = {} + + if (width != null) dimensionProps.width = width + if (height != null) dimensionProps.height = height + + return ( +
+ +
+ ) +} + +export default ComplexChart diff --git a/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.types.ts b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.types.ts new file mode 100644 index 000000000..1c76c34c2 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.types.ts @@ -0,0 +1,7 @@ +import type { ElementType, ComponentType } from 'react' + +export type ComplexChartProps = Omit & { + height?: number + width?: number + component: ElementType +} diff --git a/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.utils.ts b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.lazy.tsx b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.lazy.tsx new file mode 100644 index 000000000..caeea808a --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { DoughnutChartProps } from './DoughnutChart.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyDoughnutChart = lazy(async () => await import('./DoughnutChart')) + +const DoughnutChart = (props: JSX.IntrinsicAttributes & DoughnutChartProps): JSX.Element => ( + }> + + +) + +export default DoughnutChart diff --git a/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.stories.tsx b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.stories.tsx new file mode 100644 index 000000000..ce15ec8ec --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.stories.tsx @@ -0,0 +1,23 @@ +import type { JSX } from 'react' + +import type { ChartProps } from 'react-chartjs-2' + +import DoughnutChart from './DoughnutChart' + +export default { + title: '02-Molecules/DoughnutChart' +} + +const storyData: ChartProps<'doughnut'>['data'] = { + labels: ['Red', 'Blue', 'Yellow'], + datasets: [ + { + label: 'My First Dataset', + data: [300, 50, 100], + backgroundColor: ['rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 205, 86)'], + hoverOffset: 4 + } + ] +} + +export const Default = (): JSX.Element => diff --git a/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.tsx b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.tsx new file mode 100644 index 000000000..dddf0ac55 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.tsx @@ -0,0 +1,13 @@ +import type { FC } from 'react' + +import { Doughnut } from 'react-chartjs-2' + +import type { DoughnutChartProps } from './DoughnutChart.types' + +const DoughnutChart: FC = ({ width, height, ...doughnutProps }) => ( +
+ +
+) + +export default DoughnutChart diff --git a/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.types.ts b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.types.ts new file mode 100644 index 000000000..18944e0f1 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.types.ts @@ -0,0 +1,6 @@ +import type { ChartProps } from 'react-chartjs-2' + +export interface DoughnutChartProps extends ChartProps<'doughnut'> { + height?: number | string + width?: number | string +} diff --git a/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.utils.ts b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.lazy.tsx b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.lazy.tsx new file mode 100644 index 000000000..85cc4b500 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { EnabledCheckMarkProps } from './EnabledCheckMark.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyEnabledCheckMark = lazy(async () => await import('./EnabledCheckMark')) + +const EnabledCheckMark = (props: JSX.IntrinsicAttributes & EnabledCheckMarkProps): JSX.Element => ( + }> + + +) + +export default EnabledCheckMark diff --git a/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.stories.tsx b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.stories.tsx new file mode 100644 index 000000000..65e693497 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import EnabledCheckMark from './EnabledCheckMark' + +type StoryType = StoryObj + +const meta: Meta = { + component: EnabledCheckMark, + title: '02-Molecules/EnabledCheckMark', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + checked: true + } +} + +export const Checked: StoryType = { + args: { + checked: true + } +} + +export const Unchecked: StoryType = { + args: { + checked: false + } +} diff --git a/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.test.tsx b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.test.tsx new file mode 100644 index 000000000..c221fbefc --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import EnabledCheckMark from './EnabledCheckMark' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.tsx b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.tsx new file mode 100644 index 000000000..695b9bcbc --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.tsx @@ -0,0 +1,36 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { EnabledCheckMarkProps } from './EnabledCheckMark.types' + +import FontAwesomeIcon from '@/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon' + +const EnabledCheckMark: FC = ({ + checked, + positiveIcon, + positiveIconColour, + negativeIcon, + negativeHighlight, + negativeHighlightColour +}) => { + checked ??= false + positiveIcon ??= 'check-circle' + positiveIconColour ??= 'text-green-500' + negativeIcon ??= 'check-circle' + negativeHighlight ??= false + negativeHighlightColour ??= 'text-red-500' + + const negativeColour = negativeHighlight ? negativeHighlightColour : 'text-gray-500' + + return ( +
+ +
+ ) +} + +export default EnabledCheckMark diff --git a/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.types.ts b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.types.ts new file mode 100644 index 000000000..decd39632 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.types.ts @@ -0,0 +1,10 @@ +import type { IconProp } from '@/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.types' + +export interface EnabledCheckMarkProps { + checked?: boolean + positiveIcon?: IconProp + positiveIconColour?: string + negativeIcon?: IconProp + negativeHighlight?: boolean + negativeHighlightColour?: string +} diff --git a/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.utils.ts b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.lazy.tsx b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.lazy.tsx new file mode 100644 index 000000000..b589430c3 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormColProps } from './FormCol.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormCol = lazy(async () => await import('./FormCol')) + +const FormCol = (props: JSX.IntrinsicAttributes & FormColProps): JSX.Element => ( + }> + + +) + +export default FormCol diff --git a/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.stories.tsx b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.stories.tsx new file mode 100644 index 000000000..2a486fe58 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import FormCol from './FormCol' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormCol, + title: '02-Molecules/FormCol', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'FormCol' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.test.tsx b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.test.tsx new file mode 100644 index 000000000..139d47088 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import FormCol from './FormCol' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.tsx b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.tsx new file mode 100644 index 000000000..06f921864 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.tsx @@ -0,0 +1,21 @@ +import { useMemo, type FC } from 'react' + +import { clsx } from 'clsx' + +import type { FormColProps } from './FormCol.types' + +import Col from '@/components/01-atoms/Col/Col' + +import { coerceErrorBoolean } from '@/lib/utils' + +const FormCol: FC = ({ children, className, error }) => { + const errorValue = useMemo(() => coerceErrorBoolean(error), [error]) + + return ( + + {children} + + ) +} + +export default FormCol diff --git a/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.types.ts b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.types.ts new file mode 100644 index 000000000..db8034a03 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.types.ts @@ -0,0 +1,8 @@ +import type { ReactNode } from 'react' + +import type { ColProps } from '@/components/01-atoms/Col/Col.types' + +export interface FormColProps extends ColProps { + children?: ReactNode + error?: unknown +} diff --git a/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.utils.ts b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.lazy.tsx b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.lazy.tsx new file mode 100644 index 000000000..283f3cd31 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormRowProps } from './FormRow.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormRow = lazy(async () => await import('./FormRow')) + +const FormRow = (props: JSX.IntrinsicAttributes & FormRowProps): JSX.Element => ( + }> + + +) + +export default FormRow diff --git a/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.stories.tsx b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.stories.tsx new file mode 100644 index 000000000..6aba012c2 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import FormRow from './FormRow' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormRow, + title: '02-Molecules/FormRow', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'FormRow' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.test.tsx b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.test.tsx new file mode 100644 index 000000000..12d38dc3b --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import FormRow from './FormRow' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.tsx b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.tsx new file mode 100644 index 000000000..7f4bfc54f --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.tsx @@ -0,0 +1,21 @@ +import { useMemo, type FC } from 'react' + +import { clsx } from 'clsx' + +import type { FormRowProps } from './FormRow.types' + +import Row from '@/components/01-atoms/Row/Row' + +import { coerceErrorBoolean } from '@/lib/utils' + +const FormRow: FC = ({ children, className, error }) => { + const errorValue = useMemo(() => coerceErrorBoolean(error), [error]) + + return ( + + {children} + + ) +} + +export default FormRow diff --git a/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.types.ts b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.types.ts new file mode 100644 index 000000000..58558e47b --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.types.ts @@ -0,0 +1,8 @@ +import type { ReactNode } from 'react' + +import type { RowProps } from '@/components/01-atoms/Row/Row.types' + +export interface FormRowProps extends RowProps { + children?: ReactNode + error?: unknown +} diff --git a/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.utils.ts b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.lazy.tsx b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.lazy.tsx new file mode 100644 index 000000000..eb06f8d91 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { InputGroupProps } from './InputGroup.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyInputGroup = lazy(async () => await import('./InputGroup')) + +const InputGroup = (props: JSX.IntrinsicAttributes & InputGroupProps): JSX.Element => ( + }> + + +) + +export default InputGroup diff --git a/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.stories.tsx b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.stories.tsx new file mode 100644 index 000000000..ba2bf6911 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import InputGroup from './InputGroup' + +type StoryType = StoryObj + +const meta: Meta = { + component: InputGroup, + title: '02-Molecules/InputGroup', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'InputGroup' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.tsx b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.tsx new file mode 100644 index 000000000..5e26925e6 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.tsx @@ -0,0 +1,22 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { InputGroupProps } from './InputGroup.types' + +import Col from '@/components/01-atoms/Col/Col' +import Conditional from '@/components/01-atoms/Conditional/Conditional' +import Row from '@/components/01-atoms/Row/Row' + +const InputGroup: FC = ({ children, className, prepend }) => ( + + + +
{prepend}
+
+ {children} + +
+) + +export default InputGroup diff --git a/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.types.ts b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.types.ts new file mode 100644 index 000000000..300ca1daf --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.types.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react' + +export interface InputGroupProps { + children?: ReactNode + className?: string + prepend?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.utils.ts b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.lazy.tsx b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.lazy.tsx new file mode 100644 index 000000000..af28aaa31 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { LinkButtonProps } from './LinkButton.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyLinkButton = lazy(async () => await import('./LinkButton')) + +const LinkButton = (props: JSX.IntrinsicAttributes & LinkButtonProps): JSX.Element => ( + }> + + +) + +export default LinkButton diff --git a/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.stories.tsx b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.stories.tsx new file mode 100644 index 000000000..067868d83 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import LinkButton from './LinkButton' + +type StoryType = StoryObj + +const meta: Meta = { + component: LinkButton, + title: '02-Molecules/LinkButton', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'LinkButton' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.test.tsx b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.test.tsx new file mode 100644 index 000000000..a7b701865 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import LinkButton from './LinkButton' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render(LinkButton) + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.tsx b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.tsx new file mode 100644 index 000000000..25a7e61e0 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.tsx @@ -0,0 +1,33 @@ +import type { FC } from 'react' + +import { Link } from '@tanstack/react-router' +import { clsx } from 'clsx' + +import type { LinkButtonProps } from './LinkButton.types' + +import styles from '@/components/02-molecules/Button/Button.styles' + +const LinkButton: FC = ({ children, circle, className, small, variant, to }) => { + circle ??= false + small ??= false + variant ??= 'primary' + + return ( +
+ + {children} + +
+ ) +} + +export default LinkButton diff --git a/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.types.ts b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.types.ts new file mode 100644 index 000000000..e321bd613 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.types.ts @@ -0,0 +1,9 @@ +import type { ReactNode } from 'react' + +import type { LinkProps } from '@tanstack/react-router' + +import type { ButtonProps } from '@/components/02-molecules/Button/Button.types' + +export interface LinkButtonProps extends ButtonProps, LinkProps { + children: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.utils.ts b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/Loading/Loading.stories.tsx b/packages/frontend-react/src/components/02-molecules/Loading/Loading.stories.tsx new file mode 100644 index 000000000..7f5a52124 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Loading/Loading.stories.tsx @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from '@storybook/react' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook' + +import Loading from './Loading' + +type StoryType = StoryObj + +const meta: Meta = { + component: Loading, + title: '02-Molecules/Loading', + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'Loading' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/Loading/Loading.tsx b/packages/frontend-react/src/components/02-molecules/Loading/Loading.tsx new file mode 100644 index 000000000..db24343fd --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Loading/Loading.tsx @@ -0,0 +1,21 @@ +import type { FC } from 'react' + +import PulseLoader from 'react-spinners/PulseLoader' + +import type { LoadingProps } from './Loading.types' + +import { DefaultLoadingProps } from './Loading.utils' + +const Loading: FC = (overrideProps) => { + overrideProps ??= {} + + return ( +
+
+ +
+
+ ) +} + +export default Loading diff --git a/packages/frontend-react/src/components/02-molecules/Loading/Loading.types.ts b/packages/frontend-react/src/components/02-molecules/Loading/Loading.types.ts new file mode 100644 index 000000000..e11b93e8d --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Loading/Loading.types.ts @@ -0,0 +1,3 @@ +import type { LoaderHeightWidthRadiusProps } from 'react-spinners/helpers/props' + +export interface LoadingProps extends LoaderHeightWidthRadiusProps {} diff --git a/packages/frontend-react/src/components/02-molecules/Loading/Loading.utils.ts b/packages/frontend-react/src/components/02-molecules/Loading/Loading.utils.ts new file mode 100644 index 000000000..247bb03bf --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Loading/Loading.utils.ts @@ -0,0 +1,7 @@ +export const DefaultLoadingProps = { + height: 32, + width: 5, + radius: 5, + margin: 10, + color: '#337ab7' +} diff --git a/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.lazy.tsx b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.lazy.tsx new file mode 100644 index 000000000..398d9589c --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { NotFoundComponentProps } from './NotFoundComponent.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyNotFoundComponent = lazy(async () => await import('./NotFoundComponent')) + +const NotFoundComponent = (props: JSX.IntrinsicAttributes & NotFoundComponentProps): JSX.Element => ( + }> + + +) + +export default NotFoundComponent diff --git a/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.stories.tsx b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.stories.tsx new file mode 100644 index 000000000..cde0d9c6e --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import NotFoundComponent from './NotFoundComponent' + +type StoryType = StoryObj + +const meta: Meta = { + component: NotFoundComponent, + title: '02-Molecules/NotFoundComponent', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'NotFoundComponent' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.test.tsx b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.test.tsx new file mode 100644 index 000000000..7654e84ac --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import NotFoundComponent from './NotFoundComponent' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.tsx b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.tsx new file mode 100644 index 000000000..b1f738ff7 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.tsx @@ -0,0 +1,14 @@ +import type { FC } from 'react' + +import { Link } from '@tanstack/react-router' + +import type { NotFoundComponentProps } from './NotFoundComponent.types' + +const NotFoundComponent: FC = () => ( +
+

Not found!

+ Go home +
+) + +export default NotFoundComponent diff --git a/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.types.ts b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.types.ts new file mode 100644 index 000000000..98e9c6952 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.types.ts @@ -0,0 +1 @@ +export interface NotFoundComponentProps {} diff --git a/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.utils.ts b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.lazy.tsx b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.lazy.tsx new file mode 100644 index 000000000..e207a4cd7 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { PrivilegeIconProps } from './PrivilegeIcon.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyPrivilegeIcon = lazy(async () => await import('./PrivilegeIcon')) + +const PrivilegeIcon = (props: JSX.IntrinsicAttributes & PrivilegeIconProps): JSX.Element => ( + }> + + +) + +export default PrivilegeIcon diff --git a/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.stories.tsx b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.stories.tsx new file mode 100644 index 000000000..bb8a67d58 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook' + +import PrivilegeIcon from './PrivilegeIcon' + +type StoryType = StoryObj + +const meta: Meta = { + component: PrivilegeIcon, + title: '02-Molecules/PrivilegeIcon', + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +export const Default: StoryType = { + args: { + icon: 'key' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.test.tsx b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.test.tsx new file mode 100644 index 000000000..8206069da --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import PrivilegeIcon from './PrivilegeIcon' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.tsx b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.tsx new file mode 100644 index 000000000..637a246ae --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.tsx @@ -0,0 +1,26 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { PrivilegeIconProps } from './PrivilegeIcon.types' + +import FontAwesomeIcon from '@/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon' + +import type { IconName } from '@/types/fontawesome' + +import { getPrivilegeIconSettings } from './PrivilegeIcon.ui' + +const PrivilegeIcon: FC = ({ icon, className, size }) => { + const settings = getPrivilegeIconSettings(icon) + + return ( + + ) +} + +export default PrivilegeIcon diff --git a/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.types.ts b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.types.ts new file mode 100644 index 000000000..63d11099a --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.types.ts @@ -0,0 +1,11 @@ +import type { FontAwesomeIconProps } from '@/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.types' + +export interface PrivilegeIconProps extends Omit { + icon: string | null | undefined + className?: string +} + +export interface PrivilegeIconSettings { + icon: string | null | undefined + css: string | null | undefined +} diff --git a/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.ui.tsx b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.ui.tsx new file mode 100644 index 000000000..d19f40f52 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.ui.tsx @@ -0,0 +1,17 @@ +import type { PrivilegeIconSettings } from './PrivilegeIcon.types' + +import { isString } from '@/lib/guards/common' +import { checkValidIcon } from '@/lib/ui/fontawesome' + +export const getPrivilegeIconSettings = (icon: string | null | undefined): PrivilegeIconSettings => { + if (isString(icon) && icon !== '' && !checkValidIcon(icon)) { + return { icon: 'link-slash', css: 'opacity-50' } + } else if (!checkValidIcon(icon)) { + return { + icon: 'notdef', + css: 'opacity-25' + } + } else { + return { icon, css: null } + } +} diff --git a/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.utils.ts b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.lazy.tsx b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.lazy.tsx new file mode 100644 index 000000000..b4e8f513d --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { RedirectAdminDashboardProps } from './RedirectAdminDashboard.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyRedirectAdminDashboard = lazy(async () => await import('./RedirectAdminDashboard')) + +const RedirectAdminDashboard = (props: JSX.IntrinsicAttributes & RedirectAdminDashboardProps): JSX.Element => ( + }> + + +) + +export default RedirectAdminDashboard diff --git a/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.stories.tsx b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.stories.tsx new file mode 100644 index 000000000..6bcfe03f8 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import RedirectAdminDashboard from './RedirectAdminDashboard' + +type StoryType = StoryObj + +const meta: Meta = { + component: RedirectAdminDashboard, + title: '02-Molecules/RedirectAdminDashboard', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'RedirectAdminDashboard' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.tsx b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.tsx new file mode 100644 index 000000000..5f43528cd --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.tsx @@ -0,0 +1,9 @@ +import type { FC } from 'react' + +import { Navigate } from '@tanstack/react-router' + +import type { RedirectAdminDashboardProps } from './RedirectAdminDashboard.types' + +const RedirectAdminDashboard: FC = () => + +export default RedirectAdminDashboard diff --git a/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.types.ts b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.types.ts new file mode 100644 index 000000000..d77fcb8c0 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface RedirectAdminDashboardProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.utils.ts b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.lazy.tsx b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.lazy.tsx new file mode 100644 index 000000000..3f3b6bdff --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { RedirectLoginProps } from './RedirectLogin.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyRedirectLogin = lazy(async () => await import('./RedirectLogin')) + +const RedirectLogin = (props: JSX.IntrinsicAttributes & RedirectLoginProps): JSX.Element => ( + }> + + +) + +export default RedirectLogin diff --git a/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.stories.tsx b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.stories.tsx new file mode 100644 index 000000000..a4cfa2554 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import RedirectLogin from './RedirectLogin' + +type StoryType = StoryObj + +const meta: Meta = { + component: RedirectLogin, + title: '02-Molecules/RedirectLogin', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'RedirectLogin' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.tsx b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.tsx new file mode 100644 index 000000000..3330cf3ac --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.tsx @@ -0,0 +1,9 @@ +import type { FC } from 'react' + +import { Navigate } from '@tanstack/react-router' + +import type { RedirectLoginProps } from './RedirectLogin.types' + +const RedirectLogin: FC = () => + +export default RedirectLogin diff --git a/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.types.ts b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.types.ts new file mode 100644 index 000000000..755410148 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface RedirectLoginProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.utils.ts b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.lazy.tsx b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.lazy.tsx new file mode 100644 index 000000000..766c56bd0 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { RedirectRootProps } from './RedirectRoot.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyRedirectRoot = lazy(async () => await import('./RedirectRoot')) + +const RedirectRoot = (props: JSX.IntrinsicAttributes & RedirectRootProps): JSX.Element => ( + }> + + +) + +export default RedirectRoot diff --git a/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.stories.tsx b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.stories.tsx new file mode 100644 index 000000000..46360b6fb --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import RedirectRoot from './RedirectRoot' + +type StoryType = StoryObj + +const meta: Meta = { + component: RedirectRoot, + title: '02-Molecules/RedirectRoot', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'RedirectRoot' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.tsx b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.tsx new file mode 100644 index 000000000..0be87efe5 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.tsx @@ -0,0 +1,9 @@ +import type { FC } from 'react' + +import { Navigate } from '@tanstack/react-router' + +import type { RedirectRootProps } from './RedirectRoot.types' + +const RedirectRoot: FC = () => + +export default RedirectRoot diff --git a/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.types.ts b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.types.ts new file mode 100644 index 000000000..6d7a2d3e9 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface RedirectRootProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.utils.ts b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.lazy.tsx b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.lazy.tsx new file mode 100644 index 000000000..342dd1711 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { RedirectUserDashboardProps } from './RedirectUserDashboard.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyRedirectUserDashboard = lazy(async () => await import('./RedirectUserDashboard')) + +const RedirectUserDashboard = (props: JSX.IntrinsicAttributes & RedirectUserDashboardProps): JSX.Element => ( + }> + + +) + +export default RedirectUserDashboard diff --git a/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.stories.tsx b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.stories.tsx new file mode 100644 index 000000000..8bf9b407e --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import RedirectUserDashboard from './RedirectUserDashboard' + +type StoryType = StoryObj + +const meta: Meta = { + component: RedirectUserDashboard, + title: '02-Molecules/RedirectUserDashboard', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'RedirectUserDashboard' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.tsx b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.tsx new file mode 100644 index 000000000..9518e2a76 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.tsx @@ -0,0 +1,9 @@ +import type { FC } from 'react' + +import { Navigate } from '@tanstack/react-router' + +import type { RedirectUserDashboardProps } from './RedirectUserDashboard.types' + +const RedirectUserDashboard: FC = () => + +export default RedirectUserDashboard diff --git a/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.types.ts b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.types.ts new file mode 100644 index 000000000..8872e1f5f --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface RedirectUserDashboardProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.utils.ts b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.lazy.tsx b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.lazy.tsx new file mode 100644 index 000000000..b47de99d6 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { SpaciousRowProps } from './SpaciousRow.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazySpaciousRow = lazy(async () => await import('./SpaciousRow')) + +const SpaciousRow = (props: JSX.IntrinsicAttributes & SpaciousRowProps): JSX.Element => ( + }> + + +) + +export default SpaciousRow diff --git a/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.stories.tsx b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.stories.tsx new file mode 100644 index 000000000..9fae05978 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import SpaciousRow from './SpaciousRow' + +type StoryType = StoryObj + +const meta: Meta = { + component: SpaciousRow, + title: '02-Molecules/SpaciousRow', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'SpaciousRow' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.test.tsx b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.test.tsx new file mode 100644 index 000000000..069976624 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import SpaciousRow from './SpaciousRow' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.tsx b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.tsx new file mode 100644 index 000000000..c105f7624 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.tsx @@ -0,0 +1,15 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { SpaciousRowProps } from './SpaciousRow.types' + +import Row from '@/components/01-atoms/Row/Row' + +const SpaciousRow: FC = ({ className, children, ...restProps }) => ( + + {children} + +) + +export default SpaciousRow diff --git a/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.types.ts b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.types.ts new file mode 100644 index 000000000..6a8cb5f4c --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.types.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react' + +import type { RowProps } from '@/components/01-atoms/Row/Row.types' + +export interface SpaciousRowProps extends RowProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.utils.ts b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.lazy.tsx b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.lazy.tsx new file mode 100644 index 000000000..48891aedf --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { TableDataCellProps } from './TableDataCell.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyTableDataCell = lazy(async () => await import('./TableDataCell')) + +const TableDataCell = (props: JSX.IntrinsicAttributes & TableDataCellProps): JSX.Element => ( + }> + + +) + +export default TableDataCell diff --git a/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.stories.tsx b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.stories.tsx new file mode 100644 index 000000000..852a4dc20 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import TableDataCell from './TableDataCell' + +type StoryType = StoryObj + +const meta: Meta = { + component: TableDataCell, + title: '02-Molecules/TableDataCell', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'TableDataCell' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.test.tsx b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.test.tsx new file mode 100644 index 000000000..67b27e31c --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import TableDataCell from './TableDataCell' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.tsx b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.tsx new file mode 100644 index 000000000..25b68d0e1 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.tsx @@ -0,0 +1,26 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { TableDataCellProps } from './TableDataCell.types' + +import Conditional from '@/components/01-atoms/Conditional/Conditional' +import Popover from '@/components/01-atoms/Popover/Popover' + +const TableDataCell: FC = ({ className, children, condition }) => { + condition ??= true + + return ( + + + {(typeof children === 'string' && children.length < 16) || typeof children !== 'string' ? ( + children + ) : ( + + )} + + + ) +} + +export default TableDataCell diff --git a/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.types.ts b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.types.ts new file mode 100644 index 000000000..3ca4a7e33 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.types.ts @@ -0,0 +1,5 @@ +import type { ConditionalProps } from '@/components/01-atoms/Conditional/Conditional.types' + +import type { CastReactElement } from '@/types/utils' + +export type TableDataCellProps = CastReactElement<'td'> & Partial diff --git a/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.utils.ts b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.lazy.tsx b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.lazy.tsx new file mode 100644 index 000000000..164f0ea77 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { ToggleProps } from './Toggle.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyToggle = lazy(async () => await import('./Toggle')) + +const Toggle = (props: JSX.IntrinsicAttributes & ToggleProps): JSX.Element => ( + }> + + +) + +export default Toggle diff --git a/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.stories.tsx b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.stories.tsx new file mode 100644 index 000000000..1c06a8e12 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import Toggle from './Toggle' + +type StoryType = StoryObj + +const meta: Meta = { + component: Toggle, + title: '02-Molecules/Toggle', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + render: () => ( + <> + Unchecked + Checked + Default and Disabled + + Unchecked and Disabled + + + Checked and Disabled + + + ) +} + +export const Unchecked = {} + +export const Checked = { + args: { checked: true } +} + +export const Disabled = { + args: { disabled: true } +} diff --git a/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.tsx b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.tsx new file mode 100644 index 000000000..dfe9e4569 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.tsx @@ -0,0 +1,61 @@ +import { useEffect, useState, type FC } from 'react' + +import { CheckCircleIcon } from '@heroicons/react/24/solid' +import clsx from 'clsx' + +import type { ToggleProps } from './Toggle.types' + +const Toggle: FC = ({ children, checked, disabled, onChange, ...restProps }) => { + disabled ??= false + + const [isChecked, setChecked] = useState(checked ?? false) + + const toggleChecked = (): void => { + if (!disabled) + if (onChange != null && checked != null) onChange(!checked) + else setChecked(!isChecked) + } + + useEffect(() => { + if (onChange != null && !disabled && checked == null) onChange(isChecked) + }, [checked, disabled, isChecked, onChange]) + + useEffect(() => { + if (checked != null) setChecked(checked) + }, [checked]) + + return ( + + ) +} + +export default Toggle diff --git a/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.types.ts b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.types.ts new file mode 100644 index 000000000..96b9e7cef --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.types.ts @@ -0,0 +1,9 @@ +import type { ReactNode } from 'react' + +export interface ToggleProps { + children?: ReactNode + checked?: boolean + disabled?: boolean + onChange?: (val: boolean) => void + id?: string +} diff --git a/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.utils.ts b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.lazy.tsx b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.lazy.tsx new file mode 100644 index 000000000..08dd94227 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { UnderConstructionBannerProps } from './UnderConstructionBanner.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyUnderConstructionBanner = lazy(async () => await import('./UnderConstructionBanner')) + +const UnderConstructionBanner = (props: JSX.IntrinsicAttributes & UnderConstructionBannerProps): JSX.Element => ( + }> + + +) + +export default UnderConstructionBanner diff --git a/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.stories.tsx b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.stories.tsx new file mode 100644 index 000000000..18afa4170 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import UnderConstructionBanner from './UnderConstructionBanner' + +type StoryType = StoryObj + +const meta: Meta = { + component: UnderConstructionBanner, + title: '02-Molecules/UnderConstructionBanner', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'UnderConstructionBanner' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.tsx b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.tsx new file mode 100644 index 000000000..f4e281dae --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.tsx @@ -0,0 +1,31 @@ +import type { FC } from 'react' + +import type { UnderConstructionBannerProps } from './UnderConstructionBanner.types' + +import Col from '@/components/01-atoms/Col/Col' +import Row from '@/components/01-atoms/Row/Row' + +const UnderConstructionBanner: FC = () => ( +
+ + + +
+
+ 90s-style animated construction GIF/JIF +
+
+

Under Construction

+
+
+ + +
+
+) + +export default UnderConstructionBanner diff --git a/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.types.ts b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.types.ts new file mode 100644 index 000000000..dd0aaefc3 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface UnderConstructionBannerProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.utils.ts b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.lazy.tsx b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.lazy.tsx new file mode 100644 index 000000000..2d08ddb5f --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { UserGuardProps } from './UserGuard.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyUserGuard = lazy(async () => await import('./UserGuard')) + +const UserGuard = (props: JSX.IntrinsicAttributes & UserGuardProps): JSX.Element => ( + }> + + +) + +export default UserGuard diff --git a/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.stories.tsx b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.stories.tsx new file mode 100644 index 000000000..0b2787e0e --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import UserGuard from './UserGuard' + +type StoryType = StoryObj + +const meta: Meta = { + component: UserGuard, + title: '02-Molecules/UserGuard', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'UserGuard' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.tsx b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.tsx new file mode 100644 index 000000000..66e200524 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.tsx @@ -0,0 +1,23 @@ +import type { FC } from 'react' + +import { Navigate, useLocation } from '@tanstack/react-router' + +import type { UserGuardProps } from './UserGuard.types' + +import useAuth from '@/lib/hooks/useAuth' + +const UserGuard: FC = ({ children }) => { + const { isAuthenticated } = useAuth() + + const pathname = useLocation({ + select: (location) => location.pathname + }) + + if (!isAuthenticated) { + return + } + + return <>{children} +} + +export default UserGuard diff --git a/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.types.ts b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.types.ts new file mode 100644 index 000000000..7687ef22a --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface UserGuardProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.utils.ts b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.lazy.tsx b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.lazy.tsx new file mode 100644 index 000000000..f3238e5a9 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { UserProfileCardProps } from './UserProfileCard.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyUserProfileCard = lazy(async () => await import('./UserProfileCard')) + +const UserProfileCard = (props: JSX.IntrinsicAttributes & UserProfileCardProps): JSX.Element => ( + }> + + +) + +export default UserProfileCard diff --git a/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.stories.tsx b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.stories.tsx new file mode 100644 index 000000000..dadd37843 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import UserProfileCard from './UserProfileCard' + +type StoryType = StoryObj + +const meta: Meta = { + component: UserProfileCard, + title: '02-Molecules/UserProfileCard', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'UserProfileCard' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.tsx b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.tsx new file mode 100644 index 000000000..bb8151e38 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.tsx @@ -0,0 +1,38 @@ +import type { FC } from 'react' + +import { Link } from '@tanstack/react-router' +import MD5 from 'crypto-js/md5' + +import type { UserProfileCardProps } from './UserProfileCard.types' + +import useAuth from '@/lib/hooks/useAuth' + +const UserProfileCard: FC = () => { + const { isAuthenticated, currentUser } = useAuth() + + if (!isAuthenticated || currentUser == null) { + return Loading... + } else { + const userEmail = currentUser.email + + const emailHash: string = MD5(userEmail).toString() + + const gravatarUrl = `https://www.gravatar.com/avatar/${emailHash}?s=64&d=identicon` + + return ( +
+
+ gravatar +
+
+
+ @{currentUser.username} +
+
{currentUser.membership?.title}
+
+
+ ) + } +} + +export default UserProfileCard diff --git a/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.types.ts b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.types.ts new file mode 100644 index 000000000..767968880 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface UserProfileCardProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.utils.ts b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/03-particles/Card/Card.stories.tsx b/packages/frontend-react/src/components/03-particles/Card/Card.stories.tsx new file mode 100644 index 000000000..f0e1d2720 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/Card.stories.tsx @@ -0,0 +1,15 @@ +import type { JSX } from 'react' + +import Card from './Card' + +export default { + title: '03-Particles/Card' +} + +export const Default = (): JSX.Element => ( + + Title + Body + Footer + +) diff --git a/packages/frontend-react/src/components/03-particles/Card/Card.tsx b/packages/frontend-react/src/components/03-particles/Card/Card.tsx new file mode 100644 index 000000000..061e2202a --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/Card.tsx @@ -0,0 +1,19 @@ +import type { CardComponent } from './Card.types' + +import Body from './CardBody/CardBody' +import Container from './CardContainer/CardContainer' +import Footer from './CardFooter/CardFooter' +import Header from './CardHeader/CardHeader' + +// @ts-expect-error Property 'Container' does not exist on type 'FC'. +Container.Body = Body +// @ts-expect-error Property 'Container' does not exist on type 'FC'. +Container.Container = Container +// @ts-expect-error Property 'Container' does not exist on type 'FC'. +Container.Footer = Footer +// @ts-expect-error Property 'Container' does not exist on type 'FC'. +Container.Header = Header + +export default Container as CardComponent + +export { Body, Container, Footer, Header } diff --git a/packages/frontend-react/src/components/03-particles/Card/Card.types.ts b/packages/frontend-react/src/components/03-particles/Card/Card.types.ts new file mode 100644 index 000000000..c368628e4 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/Card.types.ts @@ -0,0 +1,13 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { FC } from 'react' + +import type { CardBodyProps } from './CardBody/CardBody.types' +import type { CardContainerProps } from './CardContainer/CardContainer.types' +import type { CardFooterProps } from './CardFooter/CardFooter.types' +import type { CardHeaderProps } from './CardHeader/CardHeader.types' + +export type CardComponent = FC & { + Body: FC + Header: FC + Footer: FC +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.lazy.tsx b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.lazy.tsx new file mode 100644 index 000000000..f44aa8dc4 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { CardBodyProps } from './CardBody.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyCardBody = lazy(async () => await import('./CardBody')) + +const CardBody = (props: JSX.IntrinsicAttributes & CardBodyProps): JSX.Element => ( + }> + + +) + +export default CardBody diff --git a/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.stories.tsx b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.stories.tsx new file mode 100644 index 000000000..74f28e559 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import CardBody from './CardBody' + +type StoryType = StoryObj + +const meta: Meta = { + component: CardBody, + title: '03-Particles/Card/CardBody', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'CardBody' + } +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.tsx b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.tsx new file mode 100644 index 000000000..c5070e15e --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.tsx @@ -0,0 +1,13 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { CardBodyProps } from './CardBody.types' + +const CardBody: FC = ({ children, className }) => ( +
+ {children} +
+) + +export default CardBody diff --git a/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.types.ts b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.types.ts new file mode 100644 index 000000000..609ec56ee --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.types.ts @@ -0,0 +1,6 @@ +import type { ReactNode } from 'react' + +export interface CardBodyProps { + children?: ReactNode + className?: string +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.utils.ts b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.lazy.tsx b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.lazy.tsx new file mode 100644 index 000000000..b9ded66eb --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { CardContainerProps } from './CardContainer.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyCardContainer = lazy(async () => await import('./CardContainer')) + +const CardContainer = (props: JSX.IntrinsicAttributes & CardContainerProps): JSX.Element => ( + }> + + +) + +export default CardContainer diff --git a/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.stories.tsx b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.stories.tsx new file mode 100644 index 000000000..3529af2af --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import CardContainer from './CardContainer' + +type StoryType = StoryObj + +const meta: Meta = { + component: CardContainer, + title: '03-Particles/Card/CardContainer', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'CardContainer' + } +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.tsx b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.tsx new file mode 100644 index 000000000..ae03d283e --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.tsx @@ -0,0 +1,21 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { CardContainerProps } from './CardContainer.types' + +const CardContainer: FC = ({ children, className, error, ...restProps }) => { + error ??= false + + return ( +
+ {children} +
+ ) +} + +export default CardContainer diff --git a/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.types.ts b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.types.ts new file mode 100644 index 000000000..82166e932 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.types.ts @@ -0,0 +1,10 @@ +import type { MouseEventHandler, ReactNode } from 'react' + +export interface CardContainerProps { + children?: ReactNode + className?: string + + error?: boolean + + onMouseLeave?: MouseEventHandler +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.utils.ts b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.lazy.tsx b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.lazy.tsx new file mode 100644 index 000000000..2362cc418 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { CardFooterProps } from './CardFooter.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyCardFooter = lazy(async () => await import('./CardFooter')) + +const CardFooter = (props: JSX.IntrinsicAttributes & CardFooterProps): JSX.Element => ( + }> + + +) + +export default CardFooter diff --git a/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.stories.tsx b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.stories.tsx new file mode 100644 index 000000000..d7aec47ec --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import CardFooter from './CardFooter' + +type StoryType = StoryObj + +const meta: Meta = { + component: CardFooter, + title: '03-Particles/Card/CardFooter', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'CardFooter' + } +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.tsx b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.tsx new file mode 100644 index 000000000..c06dbe2c1 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.tsx @@ -0,0 +1,25 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { CardFooterProps } from './CardFooter.types' + +const CardFooter: FC = ({ children, className, noGrid }) => { + noGrid ??= false + + return ( +
/^justify-/.test(c)) === false ? 'justify-around' : null + ])} + data-testid='CardFooter' + > + {children} +
+ ) +} + +export default CardFooter diff --git a/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.types.ts b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.types.ts new file mode 100644 index 000000000..8f49f7ca2 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.types.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react' + +export interface CardFooterProps { + children?: ReactNode + className?: string + noGrid?: boolean +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.utils.ts b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.lazy.tsx b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.lazy.tsx new file mode 100644 index 000000000..8e4ba40d0 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { CardHeaderProps } from './CardHeader.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyCardHeader = lazy(async () => await import('./CardHeader')) + +const CardHeader = (props: JSX.IntrinsicAttributes & CardHeaderProps): JSX.Element => ( + }> + + +) + +export default CardHeader diff --git a/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.stories.tsx b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.stories.tsx new file mode 100644 index 000000000..b10eeb7f7 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import CardHeader from './CardHeader' + +type StoryType = StoryObj + +const meta: Meta = { + component: CardHeader, + title: '03-Particles/Card/CardHeader', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'CardHeader' + } +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.tsx b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.tsx new file mode 100644 index 000000000..9e3eb46fe --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.tsx @@ -0,0 +1,13 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { CardHeaderProps } from './CardHeader.types' + +const CardHeader: FC = ({ children, className }) => ( +
+ {children} +
+) + +export default CardHeader diff --git a/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.types.ts b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.types.ts new file mode 100644 index 000000000..27dab23f3 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.types.ts @@ -0,0 +1,6 @@ +import type { ReactNode } from 'react' + +export interface CardHeaderProps { + children?: ReactNode + className?: string +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.utils.ts b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.lazy.tsx b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.lazy.tsx new file mode 100644 index 000000000..b407d3b4f --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { CreateUserModalProps } from './CreateUserModal.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyCreateUserModal = lazy(async () => await import('./CreateUserModal')) + +const CreateUserModal = (props: JSX.IntrinsicAttributes & CreateUserModalProps): JSX.Element => ( + }> + + +) + +export default CreateUserModal diff --git a/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.stories.tsx b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.stories.tsx new file mode 100644 index 000000000..503a194f1 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import CreateUserModal from './CreateUserModal' + +type StoryType = StoryObj + +const meta: Meta = { + component: CreateUserModal, + title: '03-Particles/CreateUserModal', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'CreateUserModal' + } +} diff --git a/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.test.tsx b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.test.tsx new file mode 100644 index 000000000..c819df86d --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import CreateUserModal from './CreateUserModal' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.tsx b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.tsx new file mode 100644 index 000000000..47419ee1a --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.tsx @@ -0,0 +1,15 @@ +import type { FC } from 'react' + +import type { CreateUserModalProps } from './CreateUserModal.types' + +import Button from '@/components/02-molecules/Button/Button' + +const CreateUserModal: FC = () => ( +
+ +
+) + +export default CreateUserModal diff --git a/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.types.ts b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.types.ts new file mode 100644 index 000000000..4c226d32e --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface CreateUserModalProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.utils.ts b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControl.lazy.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.lazy.tsx new file mode 100644 index 000000000..4737b2d77 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormControlProps } from './FormControl.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormControl = lazy(async () => await import('./FormControl')) + +const FormControl = (props: JSX.IntrinsicAttributes & FormControlProps): JSX.Element => ( + }> + + +) + +export default FormControl diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControl.module.css b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.module.css new file mode 100644 index 000000000..134ef3bcc --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.module.css @@ -0,0 +1,52 @@ +.Wrapper { + @apply w-full; +} + +.Container { + @apply flex min-h-8 w-full animate-btn flex-row flex-nowrap align-middle leading-6 text-gray-500 outline-none; +} + +.Main:disabled { + @apply bg-slate-100 text-black; +} + +.Main { + @apply w-available animate-btn rounded-lg border-2 border-gray-200 bg-white px-1 py-0.5 align-middle text-gray-800 outline-none; +} + +.PreContent { + @apply h-8 max-w-12 rounded-lg rounded-l rounded-r-none border-y-2 border-l-2 border-r border-gray-200 border-r-slate-200 bg-slate-100 px-2 py-0.5 outline-none; +} + +.PostContent { + @apply h-8 max-w-12 rounded-lg rounded-l-none border-y-2 border-r-2 border-gray-200 border-l-slate-200 bg-slate-100 px-2 py-0.5 text-center outline-none; +} + +.WithPreContent > .Main { + @apply rounded-l-none border-l-0; +} + +.WithPostContent > .Main { + @apply rounded-r-none border-r-0; +} + +.Focus > .Main, +.Focus > .PreContent, +.Focus > .PostContent { + @apply shadow-form-control; +} + +.WithError > .Main, +.WithError > .Main, +.WithError > .PostContent, +.WithError > .PreContent { + @apply shadow-form-error; +} + +.ResetInset { + @apply -ml-6 mt-1.5 h-6 w-6 text-gray-500/50; +} + +.PopoutError { + @apply relative bottom-8 left-16 z-above-and-beyond w-fit text-nowrap rounded-lg border border-slate-500 bg-white/100 p-1 text-center shadow-form-error; +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControl.stories.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.stories.tsx new file mode 100644 index 000000000..7b6895ab2 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.stories.tsx @@ -0,0 +1,380 @@ +import { useMemo } from 'react' + +import { zodResolver } from '@hookform/resolvers/zod' +import { FormProvider, useForm, type UseFormReturn } from 'react-hook-form' +import { z } from 'zod' + +import type { Meta, StoryObj } from '@storybook/react' + +import Col from '@/components/01-atoms/Col/Col' +import FontAwesomeIcon from '@/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon' +import Row from '@/components/01-atoms/Row/Row' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook' +import { zEmailAddress, zPasswordField, zString, zUserPin } from '@/lib/validators/common' + +import FormControl from './FormControl' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormControl, + title: '03-Particles/FormControl', + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +const FormControlStorySchema = z.object({ + field1: zString, + field2: zString, + field3: zString, + field4: zString, + field5: zPasswordField, + field6: zEmailAddress, + field7: zEmailAddress, + field8: zUserPin, + field9: zUserPin, + field10: zUserPin, + field11: z.union([z.literal('option1'), z.literal('option2'), z.literal('option3'), z.literal('option4')]), + field12: zString.min(3) +}) + +type FormControlStoryForm = z.infer + +const FormControlStoryDefaultValues: FormControlStoryForm = { + field1: '', + field2: '', + field3: '', + field4: '', + field5: 'password', + field6: 'user@example.com', + field7: 'user@example.com', + field8: '1234', + field9: '1234', + field10: '1234', + field11: 'option1', + field12: 'text area' +} + +const useStoryForm = (): UseFormReturn => { + return useForm({ + resolver: zodResolver(FormControlStorySchema), + mode: 'onChange', + defaultValues: FormControlStoryDefaultValues + }) +} + +export const Default: StoryType = { + render: () => { + const form = useStoryForm() + + const errors = useMemo(() => form.formState.errors, [form.formState.errors]) + + return ( + + + +

Unspecified

+ +
+ + + + + + + + +

Disabled

+ +
+ + + + + + + + +

Text

+ +
+ + + + + + + + +

Text with Reset

+ +
+ + + { + // form.setValue('field4', '') + }} + error={errors.field4 != null} + /> + + + + + +

Password

+ +
+ + + + + + + + +

Password

+ +
+ + + } + /> + + + + + +

Password

+ +
+ + + + + + + + +

Password

+ +
+ + + } + infoButton={{ title: 'Password', children: 'Password Field' }} + /> + + + + + +

Email

+ +
+ + + } + placeholder='user@example.com' + error={errors.field6 != null} + /> + + + + + +

Email with Info Button

+ +
+ + + } + placeholder='user@example.com' + infoButton={{ + title: '03-Particles/FormControl', + children: 'Info Content' + }} + error={errors.field7 != null} + /> + + + + + +

Number

+ +
+ + + + + + + + +

Pin

+ +
+ + + + + + + + +

Pin with Info Button

+ +
+ + + + + + + + +

Dropdown

+ +
+ + + + + + + + +

Text Area

+ +
+ + + + + +
+ ) + } +} + +export const Normal: StoryType = { + args: { formKey: 'field3', formType: 'text' } +} + +export const PinWithInfoButton: StoryType = { + render: () => { + const form = useStoryForm() + + return ( + + + + + + + + ) + } +} + +export const PreContent: StoryType = { + args: { + formKey: 'field6', + formType: 'email', + preContent: + } +} + +export const PinInput: StoryType = { + args: { + formKey: 'field8', + formType: 'number', + preContent: '0000' + } +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControl.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.tsx new file mode 100644 index 000000000..d04e62446 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.tsx @@ -0,0 +1,46 @@ +import type { FC } from 'react' + +import styles from './FormControl.module.css' +import { + isFormControlDropdownProps, + isFormControlPinProps, + isFormControlTextAreaProps, + type FormControlProps +} from './FormControl.types' +import FormControlDefault from './FormControlDefault/FormControlDefault' +import FormControlDropdown from './FormControlDropdown/FormControlDropdown' +import FormControlPin from './FormControlPin/FormControlPin' +import FormControlTextArea from './FormControlTextArea/FormControlTextArea' + +const FormControl: FC = (props) => { + if (props.formKey == null) + throw new Error('FormControl requires an id to work with react-hook-form: ' + JSON.stringify(props, null, '')) + + if (isFormControlDropdownProps(props)) { + return ( +
+ +
+ ) + } else if (isFormControlTextAreaProps(props)) { + return ( +
+ +
+ ) + } else if (isFormControlPinProps(props)) { + return ( +
+ +
+ ) + } else { + return ( +
+ +
+ ) + } +} + +export default FormControl diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControl.types.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.types.ts new file mode 100644 index 000000000..cc584c49f --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.types.ts @@ -0,0 +1,137 @@ +import type { ReactNode } from 'react' + +import { z } from 'zod' + +import type { InfoButtonProps } from '@/components/05-materials/InfoButton/InfoButton.types' + +import type { CastReactElement } from '@/types/utils' + +export const zFormControlDropdownOption = z.object({ name: z.string(), value: z.string() }) +export const zFormControlDropdownOptions = zFormControlDropdownOption.array() + +export const zFormTypeDefaults = z.union([ + z.literal('button'), + z.literal('checkbox'), + z.literal('color'), + z.literal('date'), + z.literal('datetime-local'), + z.literal('email'), + z.literal('file'), + z.literal('hidden'), + z.literal('image'), + z.literal('month'), + z.literal('number'), + z.literal('password'), + z.literal('radio'), + z.literal('range'), + z.literal('reset'), + z.literal('search'), + z.literal('submit'), + z.literal('tel'), + z.literal('text'), + z.literal('time'), + z.literal('url'), + z.literal('week') +]) +export const zFormTypeDropdown = z.literal('dropdown') +export const zFormTypePin = z.literal('pin') +export const zFormTypeTextArea = z.literal('textarea') + +export const zFormControlDefaultProps = z.object({ formType: zFormTypeDefaults }) +export const zFormControlPinProps = z.object({ formType: zFormTypePin }) +export const zFormControlDropdownProps = z.object({ formType: zFormTypeDropdown }) +export const zFormControlTextAreaProps = z.object({ formType: zFormTypeTextArea }) + +export type FormControlDropdownOption = z.infer +export type FormControlDropdownOptions = z.infer +export type FormTypeDefaults = z.infer +export type FormTypeDropdown = z.infer +export type FormTypePin = z.infer +export type FormTypeTextArea = z.infer + +type HTMLInputTypes = + | 'button' + | 'checkbox' + | 'color' + | 'date' + | 'datetime-local' + | 'email' + | 'file' + | 'hidden' + | 'image' + | 'month' + | 'number' + | 'password' + | 'radio' + | 'range' + | 'reset' + | 'search' + | 'submit' + | 'tel' + | 'text' + | 'time' + | 'url' + | 'week' + +type FormControlExcludeProps = 'name' | 'onBlur' | 'onChange' + +interface FormControlBaseProps { + formKey: string + error?: unknown + errorMessage?: string + reset?: () => void + // onChange?: (change: string) => void + // value?: string +} + +type BaseInputElementProps = Partial, FormControlExcludeProps>> & { + options?: never + preContent?: ReactNode + infoButton?: InfoButtonProps +} + +export type FormControlDefaultProps = BaseInputElementProps & + FormControlBaseProps & { + formType?: HTMLInputTypes + } + +export type FormControlPinProps = BaseInputElementProps & + FormControlBaseProps & { + formType: 'pin' + } + +export type FormControlDropdownProps = Partial, FormControlExcludeProps>> & + FormControlBaseProps & { + formType: 'dropdown' + options: string[] | FormControlDropdownOptions + preContent?: never + infoButton?: never + } + +export type FormControlTextAreaProps = Partial, FormControlExcludeProps>> & + FormControlBaseProps & { + formType: 'textarea' + rows?: HTMLTextAreaElement['rows'] + options?: never + preContent?: never + infoButton?: never + } + +export type FormControlProps = + | FormControlTextAreaProps + | FormControlPinProps + | FormControlDropdownProps + | FormControlDefaultProps + +export const isFormControlDefaultProps = (inp: unknown): inp is FormControlDefaultProps => + zFormControlDefaultProps.safeParse(inp).success +export const isFormControlDropdownOption = (inp: unknown): inp is FormControlDropdownOption => + zFormControlDropdownOption.safeParse(inp).success +export const isFormControlDropdownOptions = (inp: unknown): inp is FormControlDropdownOptions => + zFormControlDropdownOptions.safeParse(inp).success +export const isFormControlDropdownProps = (inp: unknown): inp is FormControlDropdownProps => + zFormControlDropdownProps.safeParse(inp).success +export const isFormControlPinProps = (inp: unknown): inp is FormControlPinProps => + zFormControlPinProps.safeParse(inp).success +export const isFormControlTextAreaProps = (inp: unknown): inp is FormControlTextAreaProps => + zFormControlTextAreaProps.safeParse(inp).success diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControl.utils.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.lazy.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.lazy.tsx new file mode 100644 index 000000000..ca476642f --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormControlContainerProps } from './FormControlContainer.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormControlContainer = lazy(async () => await import('./FormControlContainer')) + +const FormControlContainer = (props: JSX.IntrinsicAttributes & FormControlContainerProps): JSX.Element => ( + }> + + +) + +export default FormControlContainer diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.module.css b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.module.css new file mode 100644 index 000000000..300020053 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.module.css @@ -0,0 +1 @@ +@import '../FormControl.module.css'; diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.stories.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.stories.tsx new file mode 100644 index 000000000..f638f0c7e --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import FormControlContainer from './FormControlContainer' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormControlContainer, + title: '03-Particles/FormControl/FormControlContainer', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'FormControlContainer' + } +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.tsx new file mode 100644 index 000000000..3da264283 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.tsx @@ -0,0 +1,42 @@ +import { useMemo, type FC } from 'react' + +import clsx from 'clsx' + +import type { FormControlContainerProps } from './FormControlContainer.types' + +import { coerceErrorBoolean } from '@/lib/utils' + +import styles from './FormControlContainer.module.css' + +const FormControlContainer: FC = ({ + className, + children, + error, + hasFocus, + hasPreContent, + hasPostContent +}) => { + hasFocus ??= false + hasPreContent ??= false + hasPostContent ??= false + + const hasError = useMemo(() => coerceErrorBoolean(error), [error]) + + return ( +
+ {children} +
+ ) +} + +export default FormControlContainer diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.types.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.types.ts new file mode 100644 index 000000000..68a71a188 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.types.ts @@ -0,0 +1,11 @@ +import type { ReactNode } from 'react' + +import type { CastReactElement } from '@/types/utils' + +export interface FormControlContainerProps extends CastReactElement<'div'> { + children?: ReactNode + error?: unknown + hasFocus?: boolean + hasPreContent?: boolean + hasPostContent?: boolean +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.utils.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.lazy.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.lazy.tsx new file mode 100644 index 000000000..145c824f1 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormControlDefaultProps } from '../FormControl.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormControlDefault = lazy(async () => await import('./FormControlDefault')) + +const FormControlDefault = (props: JSX.IntrinsicAttributes & FormControlDefaultProps): JSX.Element => ( + }> + + +) + +export default FormControlDefault diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.module.css b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.module.css new file mode 100644 index 000000000..300020053 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.module.css @@ -0,0 +1 @@ +@import '../FormControl.module.css'; diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.stories.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.stories.tsx new file mode 100644 index 000000000..77f3aee84 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import FormControlDefault from './FormControlDefault' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormControlDefault, + title: '03-Particles/FormControl/FormControlDefault', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'FormControlDefault' + } +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.tsx new file mode 100644 index 000000000..0ec26ba12 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.tsx @@ -0,0 +1,91 @@ +import { useMemo, useState, type FC } from 'react' + +import { XMarkIcon } from '@heroicons/react/16/solid' +import clsx from 'clsx' +import { useFormContext } from 'react-hook-form' + +import type { FormControlDefaultProps } from '../FormControl.types' + +import Conditional from '@/components/01-atoms/Conditional/Conditional' +import InfoButton from '@/components/05-materials/InfoButton/InfoButton' + +import FormControlContainer from '../FormControlContainer/FormControlContainer' + +import styles from './FormControlDefault.module.css' + +const FormControlDefault: FC = ({ + className, + error, + errorMessage, + formType, + formKey, + id, + infoButton, + options, + preContent, + reset, + ...restProps +}) => { + error ??= false + + formType ??= 'text' + + const hasPreContent = useMemo(() => preContent != null, [preContent]) + const hasPostContent = useMemo(() => infoButton != null, [infoButton]) + + const [hasFocus, setHasFocus] = useState(false) + + const form = useFormContext() + + const { onBlur, onChange, ...formProps } = form.register(formKey) + + return ( +
+ + + {preContent ?? ''} + + { + setHasFocus(false) + void onBlur(event) + }} + onChange={(event) => { + void onChange(event) + }} + onFocus={() => { + setHasFocus(true) + }} + {...restProps} + {...formProps} + /> + + {formType !== 'password' && reset != null && ( + + { + if (reset != null) reset() + }} + /> + + )} + + + + + + + +
+ ) +} +export default FormControlDefault diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.types.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.types.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.utils.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.lazy.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.lazy.tsx new file mode 100644 index 000000000..228e9a867 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormControlDropdownProps } from '../FormControl.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormControlDropdown = lazy(async () => await import('./FormControlDropdown')) + +const FormControlDropdown = (props: JSX.IntrinsicAttributes & FormControlDropdownProps): JSX.Element => ( + }> + + +) + +export default FormControlDropdown diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.module.css b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.module.css new file mode 100644 index 000000000..300020053 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.module.css @@ -0,0 +1 @@ +@import '../FormControl.module.css'; diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.stories.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.stories.tsx new file mode 100644 index 000000000..7ff65c9a0 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import FormControlDropdown from './FormControlDropdown' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormControlDropdown, + title: '03-Particles/FormControl/FormControlDropdown', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'FormControlDropdown' + } +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.tsx new file mode 100644 index 000000000..012318d0e --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.tsx @@ -0,0 +1,65 @@ +import { useState, type FC } from 'react' + +import clsx from 'clsx' +import { useFormContext } from 'react-hook-form' + +import { isFormControlDropdownOption, type FormControlDropdownProps } from '../FormControl.types' +import FormControlContainer from '../FormControlContainer/FormControlContainer' + +import styles from './FormControlDropdown.module.css' + +const FormControlDropdown: FC = ({ + className, + error, + formType, + formKey, + id, + infoButton, + options, + preContent, + reset, + ...restProps +}) => { + const [hasFocus, setHasFocus] = useState(false) + + const form = useFormContext() + + const { onBlur, onChange, ...formProps } = form.register(formKey) + + return ( +
+ + + +
+ ) +} + +export default FormControlDropdown diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.types.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.types.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.utils.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.lazy.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.lazy.tsx new file mode 100644 index 000000000..5835dac73 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormControlPinProps } from '../FormControl.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormControlPin = lazy(async () => await import('./FormControlPin')) + +const FormControlPin = (props: JSX.IntrinsicAttributes & FormControlPinProps): JSX.Element => ( + }> + + +) + +export default FormControlPin diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.module.css b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.module.css new file mode 100644 index 000000000..300020053 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.module.css @@ -0,0 +1 @@ +@import '../FormControl.module.css'; diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.stories.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.stories.tsx new file mode 100644 index 000000000..c207eaf22 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import FormControlPin from './FormControlPin' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormControlPin, + title: '03-Particles/FormControl/FormControlPin', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'FormControlPin' + } +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.tsx new file mode 100644 index 000000000..6e63a8bbe --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.tsx @@ -0,0 +1,78 @@ +import { useMemo, useState, type FC } from 'react' + +import clsx from 'clsx' +import { useFormContext } from 'react-hook-form' + +import type { FormControlPinProps } from '../FormControl.types' + +import Conditional from '@/components/01-atoms/Conditional/Conditional' +import InfoButton from '@/components/05-materials/InfoButton/InfoButton' + +import FormControlContainer from '../FormControlContainer/FormControlContainer' + +import styles from './FormControlPin.module.css' +import { coercePaddedPinEvent } from './FormControlPin.utils' + +const FormControlPin: FC = ({ + className, + error, + formType, + formKey, + id, + infoButton, + options, + preContent, + reset, + ...restProps +}) => { + const hasPreContent = useMemo(() => preContent != null, [preContent]) + const hasPostContent = useMemo(() => infoButton != null, [infoButton]) + + const [hasFocus, setHasFocus] = useState(false) + + const form = useFormContext() + + const { onBlur, onChange, ...formProps } = form.register(formKey) + + return ( +
+ + + {preContent ?? ''} + + { + void onBlur(coercePaddedPinEvent(event)) + setHasFocus(false) + }} + onChange={(event) => { + void onChange(coercePaddedPinEvent(event)) + }} + onFocus={() => { + setHasFocus(true) + }} + max={9999} + size={4} + maxLength={4} + {...restProps} + {...formProps} + /> + + + + + + +
+ ) +} +export default FormControlPin diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.types.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.types.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.utils.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.utils.ts new file mode 100644 index 000000000..a6f605b5b --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.utils.ts @@ -0,0 +1,13 @@ +import type { BaseSyntheticEvent, FocusEvent, ChangeEvent } from 'react' + +export const coercePaddedPinEvent = ( + event: ChangeEvent | FocusEvent +): BaseSyntheticEvent => { + const t = event.target.value.padStart(4, '0') + const v = t.slice(-4, t.length) + + const copyEvent = structuredClone(event) + copyEvent.target.value = v + + return copyEvent +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.lazy.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.lazy.tsx new file mode 100644 index 000000000..bbdc6d93b --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormControlTextAreaProps } from '../FormControl.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormControlTextArea = lazy(async () => await import('./FormControlTextArea')) + +const FormControlTextArea = (props: JSX.IntrinsicAttributes & FormControlTextAreaProps): JSX.Element => ( + }> + + +) + +export default FormControlTextArea diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.module.css b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.module.css new file mode 100644 index 000000000..300020053 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.module.css @@ -0,0 +1 @@ +@import '../FormControl.module.css'; diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.stories.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.stories.tsx new file mode 100644 index 000000000..1b5a3a715 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import FormControlTextArea from './FormControlTextArea' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormControlTextArea, + title: '03-Particles/FormControl/FormControlTextArea', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'FormControlTextArea' + } +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.tsx new file mode 100644 index 000000000..621c8d515 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.tsx @@ -0,0 +1,57 @@ +import { useState, type FC } from 'react' + +import clsx from 'clsx' +import { useFormContext } from 'react-hook-form' + +import type { FormControlTextAreaProps } from '../FormControl.types' + +import FormControlContainer from '../FormControlContainer/FormControlContainer' + +import styles from './FormControlTextArea.module.css' + +const FormControlTextArea: FC = ({ + className, + error, + formType, + formKey, + id, + infoButton, + options, + preContent, + reset, + ...restProps +}) => { + const [hasFocus, setHasFocus] = useState(false) + + const form = useFormContext() + + const { onBlur, onChange, ...formProps } = form.register(formKey) + + return ( +
+ +