From 69bbd28bb072055a8bf7a0c74c7c4aec62c6f8be Mon Sep 17 00:00:00 2001 From: hamid Date: Tue, 16 Jun 2026 01:32:43 +0330 Subject: [PATCH] init --- .claude/settings.local.json | 11 + AGENTS.md | 40 + client/.env.sample | 22 + client/.eslintrc.json | 6 + client/.gitignore | 40 + client/.prettierignore | 4 + client/.prettierrc.js | 13 + client/AGENTS.md | 66 + client/README.md | 76 + client/jest.config.ts | 27 + client/jest.setup.ts | 9 + client/next.config.mjs | 16 + client/package-lock.json | 11872 ++++++++++++++++ client/package.json | 47 + client/public/img/favicon/16x16.png | Bin 0 -> 479 bytes client/public/img/favicon/180x180.png | Bin 0 -> 9127 bytes client/public/img/favicon/192x192.png | Bin 0 -> 9147 bytes client/public/img/favicon/32x32.png | Bin 0 -> 923 bytes client/public/img/favicon/512x512.png | Bin 0 -> 32040 bytes client/public/img/logo.svg | 2 + client/public/robots.txt | 5 + client/public/site.webmanifest | 22 + client/src/app/about/page.tsx | 19 + client/src/app/auth/login/LoginForm.tsx | 42 + client/src/app/auth/login/page.tsx | 21 + client/src/app/auth/page.tsx | 13 + client/src/app/auth/signup/page.tsx | 9 + client/src/app/favicon.ico | Bin 0 -> 15406 bytes client/src/app/globals.css | 18 + client/src/app/home/page.tsx | 24 + client/src/app/layout.tsx | 35 + client/src/app/me/page.tsx | 18 + client/src/app/page.tsx | 3 + client/src/components/UserInfo/UserInfo.tsx | 44 + client/src/components/UserInfo/index.tsx | 4 + .../common/AppAlert/AppAlert.test.tsx | 55 + .../components/common/AppAlert/AppAlert.tsx | 18 + .../src/components/common/AppAlert/index.tsx | 3 + .../common/AppButton/AppButton.test.tsx | 155 + .../components/common/AppButton/AppButton.tsx | 92 + .../src/components/common/AppButton/index.tsx | 3 + .../common/AppIcon/AppIcon.test.tsx | 64 + .../src/components/common/AppIcon/AppIcon.tsx | 51 + .../src/components/common/AppIcon/config.ts | 56 + .../common/AppIcon/icons/CurrencyIcon.tsx | 29 + .../common/AppIcon/icons/PencilIcon.tsx | 29 + .../common/AppIcon/icons/YellowPlanIcon.tsx | 125 + .../src/components/common/AppIcon/index.tsx | 3 + client/src/components/common/AppIcon/utils.ts | 11 + .../AppIconButton/AppIconButton.test.tsx | 126 + .../common/AppIconButton/AppIconButton.tsx | 98 + .../components/common/AppIconButton/index.tsx | 3 + .../common/AppImage/AppImage.test.tsx | 55 + .../components/common/AppImage/AppImage.tsx | 23 + .../src/components/common/AppImage/index.tsx | 3 + .../common/AppLink/AppLink.test.tsx | 229 + .../common/AppLink/AppLinkNextNavigation.tsx | 136 + .../src/components/common/AppLink/index.tsx | 4 + .../common/AppLoading/AppLoading.tsx | 36 + .../components/common/AppLoading/index.tsx | 4 + .../src/components/common/ErrorBoundary.tsx | 61 + client/src/components/common/index.tsx | 10 + client/src/components/config.ts | 35 + client/src/components/index.tsx | 5 + client/src/config.ts | 15 + client/src/hooks/auth.ts | 32 + client/src/hooks/event.ts | 17 + client/src/hooks/index.ts | 3 + client/src/hooks/layout.ts | 76 + client/src/hooks/useWindowSize.ts | 40 + client/src/layout/CurrentLayout.tsx | 15 + client/src/layout/PrivateLayout.tsx | 49 + client/src/layout/PublicLayout.tsx | 72 + client/src/layout/TopBarAndSideBarLayout.tsx | 129 + client/src/layout/components/BottomBar.tsx | 39 + client/src/layout/components/SideBar.tsx | 97 + .../src/layout/components/SideBarNavItem.tsx | 45 + .../src/layout/components/SideBarNavList.tsx | 35 + client/src/layout/components/TopBar.tsx | 46 + client/src/layout/components/index.tsx | 5 + client/src/layout/config.ts | 21 + client/src/layout/index.tsx | 6 + client/src/store/AppReducer.ts | 46 + client/src/store/AppStore.tsx | 81 + client/src/store/config.ts | 16 + client/src/store/index.tsx | 3 + .../src/theme/MuiThemeProviderForNextJs.tsx | 12 + client/src/theme/ThemeProvider.tsx | 43 + client/src/theme/colors.ts | 27 + client/src/theme/dark.ts | 18 + client/src/theme/index.ts | 10 + client/src/theme/light.ts | 18 + client/src/utils/environment.ts | 53 + client/src/utils/index.ts | 7 + client/src/utils/localStorage.ts | 56 + client/src/utils/navigation.ts | 55 + client/src/utils/sessionStorage.ts | 56 + client/src/utils/sleep.ts | 9 + client/src/utils/text.ts | 49 + client/src/utils/type.ts | 12 + client/tsconfig.json | 27 + server/.gitattributes | 63 + server/.gitignore | 342 + server/AGENTS.md | 109 + server/CleanArcTemplate.sln | 139 + server/Directory.Packages.props | 57 + server/Dockerfile | 36 + server/LICENSE.md | 21 + server/README.md | 121 + server/docker-compose.yml | 22 + .../CleanArc.Web.Api/CleanArc.Web.Api.csproj | 26 + .../V1/Admin/AdminManagerController.cs | 37 + .../V1/Admin/OrderManagementController.cs | 36 + .../V1/Admin/RoleManagerController.cs | 67 + .../V1/Admin/UserManagementController.cs | 36 + .../Controllers/V1/Order/OrderController.cs | 52 + .../V1/UserManagement/UserController.cs | 88 + server/src/API/CleanArc.Web.Api/Program.cs | 118 + .../Properties/launchSettings.json | 31 + .../appsettings.Development.json | 12 + .../src/API/CleanArc.Web.Api/appsettings.json | 20 + .../ProducesOkApiResponseTypeAttribute.cs | 39 + .../BaseController/BaseController.cs | 52 + .../CleanArc.WebFramework.csproj | 23 + .../BadRequestResultEndpointFilter.cs | 28 + .../ModelStateValidationEndpointFilter.cs | 46 + .../NotFoundResultEndpointFilter.cs | 28 + .../EndpointFilters/OkResultEndpointFilter.cs | 28 + .../Filters/ApiResultFilterAttribute.cs | 65 + .../BadRequestResultFilterAttribute.cs | 37 + .../Filters/ContentResultFilterAttribute.cs | 15 + .../Filters/ModelStateValidationAttribute.cs | 62 + .../Filters/NotFoundResultAttribute.cs | 24 + .../Filters/OkResultAttribute.cs | 28 + .../Filters/ServerErrorResult.cs | 22 + .../Middlewares/ExceptionHandler.cs | 63 + .../ServiceCollectionExtension.cs | 45 + .../Swagger/ApiVersionDocumentProcessor.cs | 25 + .../Swagger/ApplySummariesOperationFilter.cs | 122 + .../CustomTokenRequiredOperationFilter.cs | 36 + ...quireTokenWithoutAuthorizationAttribute.cs | 11 + .../Swagger/SwaggerConfigurationExtensions.cs | 69 + .../CleanArc.Web.Plugins.Grpc.csproj | 26 + .../GrpcPluginStartup.cs | 39 + .../ProtoModels/OrderGrpcServiceModels.proto | 17 + .../ProtoModels/UserGrpcServiceModels.proto | 44 + .../Services/OrderGrpcServices.cs | 46 + .../Services/UserGrpcServices.cs | 83 + .../CleanArc.Application.csproj | 23 + .../Common/LoggingBehavior.cs | 34 + .../Common/MetricsBehaviour.cs | 33 + .../Common/ValidateCommandBehavior.cs | 40 + .../Contracts/IJwtService.cs | 13 + .../Contracts/Identity/IAppUserManager.cs | 31 + .../Contracts/Identity/IRoleManagerService.cs | 16 + .../Contracts/Persistence/IOrderRepository.cs | 12 + .../Contracts/Persistence/IUnitOfWork.cs | 9 + .../IUserRefreshTokenRepository.cs | 11 + .../AddAdminCommand.Handler.cs | 44 + .../AddAdminCommand/AddAdminCommand.cs | 31 + .../GetToken/AdminGetTokenQuery.Handler.cs | 47 + .../GetToken/AdminGetTokenQuery.Result.cs | 5 + .../Queries/GetToken/AdminGetTokenQuery.cs | 27 + .../Order/Commands/AddOrderCommand.Handler.cs | 25 + .../Order/Commands/AddOrderCommand.cs | 25 + .../DeleteUserOrdersCommand.Handler.cs | 15 + .../Order/Commands/DeleteUserOrdersCommand.cs | 6 + .../UpdateUserOrderCommand.Handler.cs | 25 + .../Order/Commands/UpdateUserOrderCommand.cs | 23 + .../Queries/GetAllOrders/GetAllOrdersQuery.cs | 6 + .../GetAllOrders/GetAllOrdersQueryHandler.cs | 20 + .../GetAllOrders/GetAllOrdersQueryResult.cs | 19 + .../GetUserOrdersQueryHandler.cs | 21 + .../GetUserOrders/GetUserOrdersQueryModel.cs | 6 + .../GetUserOrders/GetUsersQueryResultModel.cs | 3 + .../AddRoleCommand/AddRoleCommand.Handler.cs | 24 + .../Commands/AddRoleCommand/AddRoleCommand.cs | 22 + .../UpdateRoleClaimsCommand.Handler.cs | 21 + .../UpdateRoleClaimsCommand.cs | 7 + .../GetAllRolesQuery.Handler.cs | 22 + .../GetAllRolesQuery.Response.cs | 3 + .../GetAllRolesQuery/GetAllRolesQuery.cs | 6 + .../GetAuthorizableRoutesQuery.Handler.cs | 24 + .../GetAuthorizableRoutesQuery.Response.cs | 3 + .../GetAuthorizableRoutesQuery.cs | 6 + .../Create/UserCreateCommand.Handler.cs | 54 + .../Create/UserCreateCommand.Result.cs | 6 + .../Commands/Create/UserCreateCommand.cs | 50 + .../RefreshUserTokenCommand.Handler.cs | 21 + .../RefreshUserTokenCommand.cs | 22 + .../RequestLogoutCommand.Handler.cs | 22 + .../RequestLogout/RequestLogoutCommand.cs | 6 + .../GenerateUserTokenQuery.Handler.cs | 33 + .../GenerateUserTokenQuery.cs | 28 + .../Queries/GetUsers/GetUsersQuery.Handler.cs | 22 + .../GetUsers/GetUsersQuery.Response.cs | 11 + .../Users/Queries/GetUsers/GetUsersQuery.cs | 6 + .../PasswordUserTokenRequestQuery.Handler.cs | 27 + .../PasswordUserTokenRequestQuery.cs | 24 + .../UserTokenRequestQuery.Handler.cs | 30 + .../UserTokenRequestQuery.Response.cs | 6 + .../TokenRequest/UserTokenRequestQuery.cs | 25 + .../Models/ApiResult/ApiResult.cs | 37 + .../Models/ApiResult/ApiResultStatusCodecs.cs | 34 + .../Models/Common/OperationResult.cs | 62 + .../Models/Identity/ActionDescriptionDto.cs | 16 + .../Models/Identity/CreateRoleDto.cs | 6 + .../Models/Identity/EditRolePermissionsDto.cs | 7 + .../Models/Identity/GetRolesDto.cs | 9 + .../Models/Identity/RolePermissionDto.cs | 14 + .../Models/Jwt/AccessToken.cs | 19 + .../Models/Jwt/TokenRequest.cs | 16 + .../ServiceCollectionExtension.cs | 29 + .../CleanArc.Domain/CleanArc.Domain.csproj | 11 + .../Core/CleanArc.Domain/Common/BaseEntity.cs | 59 + .../CleanArc.Domain/Entities/Order/Order.cs | 16 + .../CleanArc.Domain/Entities/User/Role.cs | 19 + .../Entities/User/RoleClaim.cs | 16 + .../CleanArc.Domain/Entities/User/User.cs | 29 + .../Entities/User/UserClaim.cs | 9 + .../Entities/User/UserLogin.cs | 15 + .../Entities/User/UserRefreshToken.cs | 16 + .../CleanArc.Domain/Entities/User/UserRole.cs | 12 + .../Entities/User/UserToken.cs | 16 + ...leanArc.Infrastructure.CrossCutting.csproj | 20 + .../Logging/LoggingConfiguration.cs | 73 + .../CleanArc.Infrastructure.Identity.csproj | 22 + .../Identity/AppErrorDescriber.cs | 80 + .../Identity/AppUserClaimsPrincipleFactory.cs | 34 + .../Identity/DataProtection/KeyRing.cs | 78 + .../DataProtection/LookupProtector.cs | 236 + .../DataProtection/PersonalDataProtector.cs | 241 + .../ProtectorAlgorithmHelper.cs | 35 + .../Identity/Dtos/CustomIdentityConstants.cs | 8 + .../Identity/Dtos/IdentitySettings.cs | 11 + .../Extensions/CustomIdentityExtensions.cs | 55 + .../Identity/Manager/AppRoleManager.cs | 12 + .../Identity/Manager/AppSignInManager.cs | 15 + .../Identity/Manager/AppUserManager.cs | 13 + .../PermissionManager/ConstantPolicies.cs | 6 + .../DynamicPermissionRequirement.cs | 53 + .../DynamicPermissionService.cs | 23 + .../IDynamicPermissionService.cs | 8 + .../PermissionManager/RoleManagerService.cs | 214 + .../SeedDatabaseService/SeedDataBase.cs | 47 + .../Identity/Store/AppUserStore.cs | 13 + .../Identity/Store/RoleStore.cs | 13 + .../Identity/validator/AppRoleValidator.cs | 17 + .../Identity/validator/AppUserValidator.cs | 21 + .../Jwt/JwtService.cs | 122 + .../ServiceCollectionExtension.cs | 224 + .../AppUserManagerImplementation.cs | 145 + .../CleanArc.Infrastructure.Monitoring.csproj | 22 + .../HealthCheckConfigurations.cs | 38 + .../OpenTelemetryConfigurations.cs | 28 + .../PrometheusMetricsConfigurations.cs | 15 + .../ApplicationDbContext.cs | 91 + ...CleanArc.Infrastructure.Persistence.csproj | 21 + .../Configuration/OrderConfig/OrderConfig.cs | 15 + .../UserConfig/RefreshTokenConfig.cs | 15 + .../UserConfig/RoleClaimConfig.cs | 17 + .../Configuration/UserConfig/RoleConfig.cs | 13 + .../UserConfig/UserClaimConfig.cs | 15 + .../Configuration/UserConfig/UserConfig.cs | 13 + .../UserConfig/UserLoginConfig.cs | 14 + .../UserConfig/UserRoleConfig.cs | 16 + .../UserConfig/UserTokenConfig.cs | 15 + .../20210327210004_Init.Designer.cs | 374 + .../Migrations/20210327210004_Init.cs | 294 + ...4354_AddedOrderAndUserRelation.Designer.cs | 422 + ...0221205084354_AddedOrderAndUserRelation.cs | 50 + ...126140035_AddedOrderDeleteFlag.Designer.cs | 424 + .../20231126140035_AddedOrderDeleteFlag.cs | 29 + .../ApplicationDbContextModelSnapshot.cs | 421 + .../Common/BaseAsyncRepository.cs | 44 + .../Repositories/Common/UnitOfWork.cs | 28 + .../Repositories/OrderRepository.cs | 41 + .../UserRefreshTokenRepository.cs | 40 + .../ServiceCollectionExtensions.cs | 35 + .../CleanArc.SharedKernel.csproj | 19 + .../Extensions/AssemblyExtensions.cs | 19 + .../Extensions/CollectionExtensions.cs | 15 + .../Extensions/EnumExtentions.cs | 54 + .../Extensions/IdentityExtensions.cs | 43 + .../Extensions/ModelBuilderExtensions.cs | 120 + .../Extensions/ReflectionExtensions.cs | 102 + .../Extensions/RegExHelpers.cs | 15 + .../Extensions/StringExtensions.cs | 103 + .../Extensions/ValidatorExtensions.cs | 50 + .../ApplicationBaseValidationModelProvider.cs | 6 + .../Contracts/IValidatableModel.cs | 8 + ...eanArc.Test.Infrastructure.Identity.csproj | 28 + .../UserManagerTest.cs | 96 + .../Usings.cs | 1 + .../CleanArc.Tests.Setup.csproj | 34 + .../Setups/TestApplicationDbContext.cs | 23 + .../Setups/TestIdentitySetup.cs | 88 + .../src/Tests/CleanArc.Tests.Setup/Usings.cs | 1 + 298 files changed, 24728 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 AGENTS.md create mode 100644 client/.env.sample create mode 100644 client/.eslintrc.json create mode 100644 client/.gitignore create mode 100644 client/.prettierignore create mode 100644 client/.prettierrc.js create mode 100644 client/AGENTS.md create mode 100644 client/README.md create mode 100644 client/jest.config.ts create mode 100644 client/jest.setup.ts create mode 100644 client/next.config.mjs create mode 100644 client/package-lock.json create mode 100644 client/package.json create mode 100644 client/public/img/favicon/16x16.png create mode 100644 client/public/img/favicon/180x180.png create mode 100644 client/public/img/favicon/192x192.png create mode 100644 client/public/img/favicon/32x32.png create mode 100644 client/public/img/favicon/512x512.png create mode 100644 client/public/img/logo.svg create mode 100644 client/public/robots.txt create mode 100644 client/public/site.webmanifest create mode 100644 client/src/app/about/page.tsx create mode 100644 client/src/app/auth/login/LoginForm.tsx create mode 100644 client/src/app/auth/login/page.tsx create mode 100644 client/src/app/auth/page.tsx create mode 100644 client/src/app/auth/signup/page.tsx create mode 100644 client/src/app/favicon.ico create mode 100644 client/src/app/globals.css create mode 100644 client/src/app/home/page.tsx create mode 100644 client/src/app/layout.tsx create mode 100644 client/src/app/me/page.tsx create mode 100644 client/src/app/page.tsx create mode 100644 client/src/components/UserInfo/UserInfo.tsx create mode 100644 client/src/components/UserInfo/index.tsx create mode 100644 client/src/components/common/AppAlert/AppAlert.test.tsx create mode 100644 client/src/components/common/AppAlert/AppAlert.tsx create mode 100644 client/src/components/common/AppAlert/index.tsx create mode 100644 client/src/components/common/AppButton/AppButton.test.tsx create mode 100644 client/src/components/common/AppButton/AppButton.tsx create mode 100644 client/src/components/common/AppButton/index.tsx create mode 100644 client/src/components/common/AppIcon/AppIcon.test.tsx create mode 100644 client/src/components/common/AppIcon/AppIcon.tsx create mode 100644 client/src/components/common/AppIcon/config.ts create mode 100644 client/src/components/common/AppIcon/icons/CurrencyIcon.tsx create mode 100644 client/src/components/common/AppIcon/icons/PencilIcon.tsx create mode 100644 client/src/components/common/AppIcon/icons/YellowPlanIcon.tsx create mode 100644 client/src/components/common/AppIcon/index.tsx create mode 100644 client/src/components/common/AppIcon/utils.ts create mode 100644 client/src/components/common/AppIconButton/AppIconButton.test.tsx create mode 100644 client/src/components/common/AppIconButton/AppIconButton.tsx create mode 100644 client/src/components/common/AppIconButton/index.tsx create mode 100644 client/src/components/common/AppImage/AppImage.test.tsx create mode 100644 client/src/components/common/AppImage/AppImage.tsx create mode 100644 client/src/components/common/AppImage/index.tsx create mode 100644 client/src/components/common/AppLink/AppLink.test.tsx create mode 100644 client/src/components/common/AppLink/AppLinkNextNavigation.tsx create mode 100644 client/src/components/common/AppLink/index.tsx create mode 100644 client/src/components/common/AppLoading/AppLoading.tsx create mode 100644 client/src/components/common/AppLoading/index.tsx create mode 100644 client/src/components/common/ErrorBoundary.tsx create mode 100644 client/src/components/common/index.tsx create mode 100644 client/src/components/config.ts create mode 100644 client/src/components/index.tsx create mode 100644 client/src/config.ts create mode 100644 client/src/hooks/auth.ts create mode 100644 client/src/hooks/event.ts create mode 100644 client/src/hooks/index.ts create mode 100644 client/src/hooks/layout.ts create mode 100644 client/src/hooks/useWindowSize.ts create mode 100644 client/src/layout/CurrentLayout.tsx create mode 100644 client/src/layout/PrivateLayout.tsx create mode 100644 client/src/layout/PublicLayout.tsx create mode 100644 client/src/layout/TopBarAndSideBarLayout.tsx create mode 100644 client/src/layout/components/BottomBar.tsx create mode 100644 client/src/layout/components/SideBar.tsx create mode 100644 client/src/layout/components/SideBarNavItem.tsx create mode 100644 client/src/layout/components/SideBarNavList.tsx create mode 100644 client/src/layout/components/TopBar.tsx create mode 100644 client/src/layout/components/index.tsx create mode 100644 client/src/layout/config.ts create mode 100644 client/src/layout/index.tsx create mode 100644 client/src/store/AppReducer.ts create mode 100644 client/src/store/AppStore.tsx create mode 100644 client/src/store/config.ts create mode 100644 client/src/store/index.tsx create mode 100644 client/src/theme/MuiThemeProviderForNextJs.tsx create mode 100644 client/src/theme/ThemeProvider.tsx create mode 100644 client/src/theme/colors.ts create mode 100644 client/src/theme/dark.ts create mode 100644 client/src/theme/index.ts create mode 100644 client/src/theme/light.ts create mode 100644 client/src/utils/environment.ts create mode 100644 client/src/utils/index.ts create mode 100644 client/src/utils/localStorage.ts create mode 100644 client/src/utils/navigation.ts create mode 100644 client/src/utils/sessionStorage.ts create mode 100644 client/src/utils/sleep.ts create mode 100644 client/src/utils/text.ts create mode 100644 client/src/utils/type.ts create mode 100644 client/tsconfig.json create mode 100644 server/.gitattributes create mode 100644 server/.gitignore create mode 100644 server/AGENTS.md create mode 100644 server/CleanArcTemplate.sln create mode 100644 server/Directory.Packages.props create mode 100644 server/Dockerfile create mode 100644 server/LICENSE.md create mode 100644 server/README.md create mode 100644 server/docker-compose.yml create mode 100644 server/src/API/CleanArc.Web.Api/CleanArc.Web.Api.csproj create mode 100644 server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/AdminManagerController.cs create mode 100644 server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/OrderManagementController.cs create mode 100644 server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/RoleManagerController.cs create mode 100644 server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/UserManagementController.cs create mode 100644 server/src/API/CleanArc.Web.Api/Controllers/V1/Order/OrderController.cs create mode 100644 server/src/API/CleanArc.Web.Api/Controllers/V1/UserManagement/UserController.cs create mode 100644 server/src/API/CleanArc.Web.Api/Program.cs create mode 100644 server/src/API/CleanArc.Web.Api/Properties/launchSettings.json create mode 100644 server/src/API/CleanArc.Web.Api/appsettings.Development.json create mode 100644 server/src/API/CleanArc.Web.Api/appsettings.json create mode 100644 server/src/API/CleanArc.WebFramework/Attributes/ProducesOkApiResponseTypeAttribute.cs create mode 100644 server/src/API/CleanArc.WebFramework/BaseController/BaseController.cs create mode 100644 server/src/API/CleanArc.WebFramework/CleanArc.WebFramework.csproj create mode 100644 server/src/API/CleanArc.WebFramework/EndpointFilters/BadRequestResultEndpointFilter.cs create mode 100644 server/src/API/CleanArc.WebFramework/EndpointFilters/ModelStateValidationEndpointFilter.cs create mode 100644 server/src/API/CleanArc.WebFramework/EndpointFilters/NotFoundResultEndpointFilter.cs create mode 100644 server/src/API/CleanArc.WebFramework/EndpointFilters/OkResultEndpointFilter.cs create mode 100644 server/src/API/CleanArc.WebFramework/Filters/ApiResultFilterAttribute.cs create mode 100644 server/src/API/CleanArc.WebFramework/Filters/BadRequestResultFilterAttribute.cs create mode 100644 server/src/API/CleanArc.WebFramework/Filters/ContentResultFilterAttribute.cs create mode 100644 server/src/API/CleanArc.WebFramework/Filters/ModelStateValidationAttribute.cs create mode 100644 server/src/API/CleanArc.WebFramework/Filters/NotFoundResultAttribute.cs create mode 100644 server/src/API/CleanArc.WebFramework/Filters/OkResultAttribute.cs create mode 100644 server/src/API/CleanArc.WebFramework/Filters/ServerErrorResult.cs create mode 100644 server/src/API/CleanArc.WebFramework/Middlewares/ExceptionHandler.cs create mode 100644 server/src/API/CleanArc.WebFramework/ServiceConfiguration/ServiceCollectionExtension.cs create mode 100644 server/src/API/CleanArc.WebFramework/Swagger/ApiVersionDocumentProcessor.cs create mode 100644 server/src/API/CleanArc.WebFramework/Swagger/ApplySummariesOperationFilter.cs create mode 100644 server/src/API/CleanArc.WebFramework/Swagger/CustomTokenRequiredOperationFilter.cs create mode 100644 server/src/API/CleanArc.WebFramework/Swagger/RequireTokenWithoutAuthorizationAttribute.cs create mode 100644 server/src/API/CleanArc.WebFramework/Swagger/SwaggerConfigurationExtensions.cs create mode 100644 server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/CleanArc.Web.Plugins.Grpc.csproj create mode 100644 server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/GrpcPluginStartup.cs create mode 100644 server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/ProtoModels/OrderGrpcServiceModels.proto create mode 100644 server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/ProtoModels/UserGrpcServiceModels.proto create mode 100644 server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/Services/OrderGrpcServices.cs create mode 100644 server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/Services/UserGrpcServices.cs create mode 100644 server/src/Core/CleanArc.Application/CleanArc.Application.csproj create mode 100644 server/src/Core/CleanArc.Application/Common/LoggingBehavior.cs create mode 100644 server/src/Core/CleanArc.Application/Common/MetricsBehaviour.cs create mode 100644 server/src/Core/CleanArc.Application/Common/ValidateCommandBehavior.cs create mode 100644 server/src/Core/CleanArc.Application/Contracts/IJwtService.cs create mode 100644 server/src/Core/CleanArc.Application/Contracts/Identity/IAppUserManager.cs create mode 100644 server/src/Core/CleanArc.Application/Contracts/Identity/IRoleManagerService.cs create mode 100644 server/src/Core/CleanArc.Application/Contracts/Persistence/IOrderRepository.cs create mode 100644 server/src/Core/CleanArc.Application/Contracts/Persistence/IUnitOfWork.cs create mode 100644 server/src/Core/CleanArc.Application/Contracts/Persistence/IUserRefreshTokenRepository.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Admin/Commands/AddAdminCommand/AddAdminCommand.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Admin/Commands/AddAdminCommand/AddAdminCommand.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Admin/Queries/GetToken/AdminGetTokenQuery.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Admin/Queries/GetToken/AdminGetTokenQuery.Result.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Admin/Queries/GetToken/AdminGetTokenQuery.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Order/Commands/AddOrderCommand.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Order/Commands/AddOrderCommand.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Order/Commands/DeleteUserOrdersCommand.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Order/Commands/DeleteUserOrdersCommand.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Order/Commands/UpdateUserOrderCommand.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Order/Commands/UpdateUserOrderCommand.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Order/Queries/GetAllOrders/GetAllOrdersQuery.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Order/Queries/GetAllOrders/GetAllOrdersQueryHandler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Order/Queries/GetAllOrders/GetAllOrdersQueryResult.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Order/Queries/GetUserOrders/GetUserOrdersQueryHandler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Order/Queries/GetUserOrders/GetUserOrdersQueryModel.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Order/Queries/GetUserOrders/GetUsersQueryResultModel.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Role/Commands/AddRoleCommand/AddRoleCommand.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Role/Commands/AddRoleCommand/AddRoleCommand.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Role/Commands/UpdateRoleClaimsCommand/UpdateRoleClaimsCommand.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Role/Commands/UpdateRoleClaimsCommand/UpdateRoleClaimsCommand.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Role/Queries/GetAllRolesQuery/GetAllRolesQuery.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Role/Queries/GetAllRolesQuery/GetAllRolesQuery.Response.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Role/Queries/GetAllRolesQuery/GetAllRolesQuery.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Role/Queries/GetAuthorizableRoutesQuery/GetAuthorizableRoutesQuery.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Role/Queries/GetAuthorizableRoutesQuery/GetAuthorizableRoutesQuery.Response.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Role/Queries/GetAuthorizableRoutesQuery/GetAuthorizableRoutesQuery.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Commands/Create/UserCreateCommand.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Commands/Create/UserCreateCommand.Result.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Commands/Create/UserCreateCommand.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Commands/RefreshUserTokenCommand/RefreshUserTokenCommand.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Commands/RefreshUserTokenCommand/RefreshUserTokenCommand.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Commands/RequestLogout/RequestLogoutCommand.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Commands/RequestLogout/RequestLogoutCommand.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Queries/GenerateUserToken/GenerateUserTokenQuery.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Queries/GenerateUserToken/GenerateUserTokenQuery.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Queries/GetUsers/GetUsersQuery.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Queries/GetUsers/GetUsersQuery.Response.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Queries/GetUsers/GetUsersQuery.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/PasswordUserTokenRequestQuery.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/PasswordUserTokenRequestQuery.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/UserTokenRequestQuery.Handler.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/UserTokenRequestQuery.Response.cs create mode 100644 server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/UserTokenRequestQuery.cs create mode 100644 server/src/Core/CleanArc.Application/Models/ApiResult/ApiResult.cs create mode 100644 server/src/Core/CleanArc.Application/Models/ApiResult/ApiResultStatusCodecs.cs create mode 100644 server/src/Core/CleanArc.Application/Models/Common/OperationResult.cs create mode 100644 server/src/Core/CleanArc.Application/Models/Identity/ActionDescriptionDto.cs create mode 100644 server/src/Core/CleanArc.Application/Models/Identity/CreateRoleDto.cs create mode 100644 server/src/Core/CleanArc.Application/Models/Identity/EditRolePermissionsDto.cs create mode 100644 server/src/Core/CleanArc.Application/Models/Identity/GetRolesDto.cs create mode 100644 server/src/Core/CleanArc.Application/Models/Identity/RolePermissionDto.cs create mode 100644 server/src/Core/CleanArc.Application/Models/Jwt/AccessToken.cs create mode 100644 server/src/Core/CleanArc.Application/Models/Jwt/TokenRequest.cs create mode 100644 server/src/Core/CleanArc.Application/ServiceConfiguration/ServiceCollectionExtension.cs create mode 100644 server/src/Core/CleanArc.Domain/CleanArc.Domain.csproj create mode 100644 server/src/Core/CleanArc.Domain/Common/BaseEntity.cs create mode 100644 server/src/Core/CleanArc.Domain/Entities/Order/Order.cs create mode 100644 server/src/Core/CleanArc.Domain/Entities/User/Role.cs create mode 100644 server/src/Core/CleanArc.Domain/Entities/User/RoleClaim.cs create mode 100644 server/src/Core/CleanArc.Domain/Entities/User/User.cs create mode 100644 server/src/Core/CleanArc.Domain/Entities/User/UserClaim.cs create mode 100644 server/src/Core/CleanArc.Domain/Entities/User/UserLogin.cs create mode 100644 server/src/Core/CleanArc.Domain/Entities/User/UserRefreshToken.cs create mode 100644 server/src/Core/CleanArc.Domain/Entities/User/UserRole.cs create mode 100644 server/src/Core/CleanArc.Domain/Entities/User/UserToken.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.CrossCutting/CleanArc.Infrastructure.CrossCutting.csproj create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.CrossCutting/Logging/LoggingConfiguration.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/CleanArc.Infrastructure.Identity.csproj create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/AppErrorDescriber.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/AppUserClaimsPrincipleFactory.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/KeyRing.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/LookupProtector.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/PersonalDataProtector.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/ProtectorAlgorithmHelper.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Dtos/CustomIdentityConstants.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Dtos/IdentitySettings.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Extensions/CustomIdentityExtensions.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Manager/AppRoleManager.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Manager/AppSignInManager.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Manager/AppUserManager.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/ConstantPolicies.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/DynamicPermissionRequirement.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/DynamicPermissionService.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/IDynamicPermissionService.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/RoleManagerService.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/SeedDatabaseService/SeedDataBase.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Store/AppUserStore.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Store/RoleStore.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/validator/AppRoleValidator.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/validator/AppUserValidator.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/Jwt/JwtService.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/ServiceConfiguration/ServiceCollectionExtension.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Identity/UserManager/AppUserManagerImplementation.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/CleanArc.Infrastructure.Monitoring.csproj create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/Configurations/HealthCheckConfigurations.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/Configurations/OpenTelemetryConfigurations.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/Configurations/PrometheusMetricsConfigurations.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/ApplicationDbContext.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/CleanArc.Infrastructure.Persistence.csproj create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/OrderConfig/OrderConfig.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/RefreshTokenConfig.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/RoleClaimConfig.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/RoleConfig.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserClaimConfig.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserConfig.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserLoginConfig.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserRoleConfig.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserTokenConfig.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20210327210004_Init.Designer.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20210327210004_Init.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20221205084354_AddedOrderAndUserRelation.Designer.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20221205084354_AddedOrderAndUserRelation.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20231126140035_AddedOrderDeleteFlag.Designer.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20231126140035_AddedOrderDeleteFlag.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/Common/BaseAsyncRepository.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/Common/UnitOfWork.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/OrderRepository.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/UserRefreshTokenRepository.cs create mode 100644 server/src/Infrastructure/CleanArc.Infrastructure.Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs create mode 100644 server/src/Shared/CleanArc.SharedKernel/CleanArc.SharedKernel.csproj create mode 100644 server/src/Shared/CleanArc.SharedKernel/Extensions/AssemblyExtensions.cs create mode 100644 server/src/Shared/CleanArc.SharedKernel/Extensions/CollectionExtensions.cs create mode 100644 server/src/Shared/CleanArc.SharedKernel/Extensions/EnumExtentions.cs create mode 100644 server/src/Shared/CleanArc.SharedKernel/Extensions/IdentityExtensions.cs create mode 100644 server/src/Shared/CleanArc.SharedKernel/Extensions/ModelBuilderExtensions.cs create mode 100644 server/src/Shared/CleanArc.SharedKernel/Extensions/ReflectionExtensions.cs create mode 100644 server/src/Shared/CleanArc.SharedKernel/Extensions/RegExHelpers.cs create mode 100644 server/src/Shared/CleanArc.SharedKernel/Extensions/StringExtensions.cs create mode 100644 server/src/Shared/CleanArc.SharedKernel/Extensions/ValidatorExtensions.cs create mode 100644 server/src/Shared/CleanArc.SharedKernel/ValidationBase/ApplicationBaseValidationModelProvider.cs create mode 100644 server/src/Shared/CleanArc.SharedKernel/ValidationBase/Contracts/IValidatableModel.cs create mode 100644 server/src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity.csproj create mode 100644 server/src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/UserManagerTest.cs create mode 100644 server/src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/Usings.cs create mode 100644 server/src/Tests/CleanArc.Tests.Setup/CleanArc.Tests.Setup.csproj create mode 100644 server/src/Tests/CleanArc.Tests.Setup/Setups/TestApplicationDbContext.cs create mode 100644 server/src/Tests/CleanArc.Tests.Setup/Setups/TestIdentitySetup.cs create mode 100644 server/src/Tests/CleanArc.Tests.Setup/Usings.cs diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..0e5a8e6 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(rm -rf server/.template.config)", + "Bash(rm -f server/CleanArcTemplate.nuspec)", + "Bash(rm -f server/.github/workflows/package.yml)", + "Bash(rmdir server/.github/workflows)", + "Bash(rmdir server/.github)" + ] + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3a4500c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,40 @@ +# AGENTS.md — Balinyaar + +Guidance for AI coding agents working in this repository. Read this first, then the project-specific `AGENTS.md` for whichever side you are editing. + +## What this repository is + +Balinyaar is a full-stack application split into two independent projects: + +| Path | Project | Stack | Detail | +| -------------------------- | -------------- | ----------------------------------------------------------- | ------------------------------- | +| [`client/`](client/) | Web frontend | Next.js (App Router), React, TypeScript, Material UI (MUI) | [client/AGENTS.md](client/AGENTS.md) | +| [`server/`](server/) | Backend API | ASP.NET Core (.NET 10), Clean Architecture, CQRS, EF Core | [server/AGENTS.md](server/AGENTS.md) | + +The two communicate over HTTP/JSON (and optionally gRPC). The client reads the API base URL from `NEXT_PUBLIC_API_URL`; the server runs on `https://localhost:5002` by default. + +There is **no root-level build** — each project is built and run on its own. `client/` and `server/` are not part of the same package/solution. + +## Working agreements + +- **Stay within one project per change** unless the task explicitly spans both. A frontend change rarely needs server files and vice-versa. +- **Match the surrounding style.** Each project has its own conventions (see its `AGENTS.md`). Mirror the existing patterns rather than introducing new ones. +- **Run the project's own checks** before declaring work done: + - client: `npm run lint` and `npm run type` (and `npm run test:ci` if tests are touched) + - server: `dotnet build` and `dotnet test` +- **Do not reintroduce template/starter scaffolding.** This repo was derived from two open-source starters; their branding, demo/showcase pages, NuGet template packaging, and `_TITLE_`/`_DESCRIPTION_` placeholders have been intentionally removed. Don't add them back. +- **Secrets**: never commit real connection strings, keys, or tokens. Use `.env` (client) and `appsettings.*.json` / user-secrets (server). + +## Quick start + +```bash +# Frontend +cd client && npm install && npm run dev # http://localhost:3000 + +# Backend +cd server && dotnet run --project src/API/CleanArc.Web.Api/CleanArc.Web.Api.csproj # https://localhost:5002/swagger +``` + +## Naming note + +The server's C# namespaces and solution file still use the `CleanArc*` prefix (e.g. `CleanArc.Web.Api`, `CleanArcTemplate.sln`). This is the internal project naming and is **not** template branding — do not mass-rename it unless explicitly asked, as it touches every file, the `.sln`, and EF migrations. diff --git a/client/.env.sample b/client/.env.sample new file mode 100644 index 0000000..f56118c --- /dev/null +++ b/client/.env.sample @@ -0,0 +1,22 @@ +# Environment similar to NODE_ENV. +# Analytics and public resources are enabled only in "production" +# How to use: set value to "production" to get fully functional application. +NEXT_PUBLIC_ENV = development +# NEXT_PUBLIC_ENV = preview +# NEXT_PUBLIC_ENV = production + +# Enables additional debug features, no additional debug information if the variable is not set +# How to use: set value to "true" to get more debugging information, but don't do it on production. +NEXT_PUBLIC_DEBUG = true + +# Public URL of the application/website. +# How to use: Do not set any value until you need custom domain for your application. +# NEXT_PUBLIC_PUBLIC_URL = https://xxx.com +# NEXT_PUBLIC_PUBLIC_URL = https://xxx.web.app +NEXT_PUBLIC_PUBLIC_URL = http://localhost:3000 + + +# API/Backend basic URL (the CleanArc server) +NEXT_PUBLIC_API_URL = https://localhost:5002 +# NEXT_PUBLIC_API_URL = https://dev-api.domain.com +# NEXT_PUBLIC_API_URL = https://api.domain.com \ No newline at end of file diff --git a/client/.eslintrc.json b/client/.eslintrc.json new file mode 100644 index 0000000..75c45fb --- /dev/null +++ b/client/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "next/core-web-vitals", + "rules": { + "import/no-cycle": "error" + } +} diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..2252b6e --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,40 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/client/.prettierignore b/client/.prettierignore new file mode 100644 index 0000000..631ff20 --- /dev/null +++ b/client/.prettierignore @@ -0,0 +1,4 @@ +.next +node_modules +out +styles \ No newline at end of file diff --git a/client/.prettierrc.js b/client/.prettierrc.js new file mode 100644 index 0000000..98bd062 --- /dev/null +++ b/client/.prettierrc.js @@ -0,0 +1,13 @@ +module.exports = { + printWidth: 120, // max 120 chars in line, code is easy to read + useTabs: false, // use spaces instead of tabs + tabWidth: 2, // "visual width" of of the "tab" + trailingComma: 'es5', // add trailing commas in objects, arrays, etc. + semi: true, // add ; when needed + singleQuote: true, // '' for stings instead of "" + bracketSpacing: true, // import { some } ... instead of import {some} ... + arrowParens: 'always', // braces even for single param in arrow functions (a) => { } + jsxSingleQuote: false, // "" for react props, like in html + jsxBracketSameLine: false, // pretty JSX + endOfLine: 'lf', // 'lf' for linux, 'crlf' for windows, we need to use 'lf' for git +}; diff --git a/client/AGENTS.md b/client/AGENTS.md new file mode 100644 index 0000000..c2cfe7f --- /dev/null +++ b/client/AGENTS.md @@ -0,0 +1,66 @@ +# AGENTS.md — Balinyaar Web Client + +Agent-oriented guide to the frontend. For human setup/run instructions see [README.md](README.md). + +## Stack + +- **Next.js** with the **App Router** (`src/app/`), statically exported (`output: 'export'` in `next.config.mjs` → builds to `out/`) +- **React** + **TypeScript** (`strict` mode) +- **Material UI (MUI)** for components and theming (Emotion under the hood) +- **Jest** + **Testing Library** for unit tests +- ESLint (`eslint-config-next`) + Prettier + +## Commands + +| Task | Command | +| ------------ | ------------------ | +| Dev server | `npm run dev` | +| Build | `npm run build` | +| Lint | `npm run lint` | +| Type-check | `npm run type` | +| Format | `npm run format` | +| Test (watch) | `npm test` | +| Test (CI) | `npm run test:ci` | + +Always run `npm run lint` and `npm run type` after a change. Run `npm run test:ci` when you touch a component that has a `*.test.tsx`. + +## Directory map + +``` +src/ +├── app/ Routes (App Router). Each folder = a route; page.tsx = the page. +│ ├── layout.tsx Root layout: store + theme providers, global +│ ├── page.tsx "/" entry (delegates to home) +│ ├── home/ about/ Content pages +│ ├── auth/login|signup Auth pages (LoginForm is currently a stub) +│ └── me/ Authenticated user page +├── components/ +│ ├── common/ Reusable App* primitives (see below) + ErrorBoundary +│ └── UserInfo/ Logged-in user summary +├── hooks/ useIsAuthenticated, useIsMobile, event hooks, useWindowSize +├── layout/ PublicLayout / PrivateLayout + TopBar, SideBar, BottomBar, config +├── store/ Global state: React context + reducer (AppStore, AppReducer) +├── theme/ ThemeProvider, light/dark palettes, colors, MUI-for-Next bridge +└── utils/ storage (local/session), navigation, environment, text, types +``` + +## Conventions (follow these) + +- **Imports**: use the `@/*` alias for anything under `src/` (e.g. `import { AppButton } from '@/components'`). The alias is defined in `tsconfig.json`. +- **Barrel files**: most folders export through an `index.ts(x)`. Add new public exports there (e.g. a new common component is re-exported from `src/components/common/index.tsx`, which `src/components/index.tsx` re-exports). +- **Common components** live in `src/components/common//` as a folder containing `.tsx`, `index.tsx` (re-export), and an optional `.test.tsx`. Existing ones: `AppAlert`, `AppButton`, `AppIcon`, `AppIconButton`, `AppImage`, `AppLink`, `AppLoading`. Prefer reusing these over raw MUI where one exists. +- **Icons**: reference icons by string name through `AppIcon`. The name→icon map is in `src/components/common/AppIcon/config.ts`; add new icons there (custom SVGs go in `AppIcon/icons/`). +- **Navigation**: sidebar/bottom-bar items are arrays of `LinkToPage` defined inside `PublicLayout.tsx` / `PrivateLayout.tsx`. Add a route → add an entry there to surface it in the nav. +- **Two layouts**: `PrivateLayout` (after auth) and `PublicLayout` (before auth); `CurrentLayout` picks between them based on auth state. The app name lives in the `TITLE_PRIVATE` / `TITLE_PUBLIC` constants in those files. +- **Auth is a stub.** `src/hooks/auth.ts` and `src/app/auth/login/LoginForm.tsx` fake login by writing a placeholder token to session storage (`// TODO: AUTH:`). When implementing real auth, replace those spots and point requests at `NEXT_PUBLIC_API_URL` (the `server` project's JWT endpoints). +- **Theming**: use the theme/palette via MUI's `sx` / `useTheme`; don't hardcode colors. Light/dark values are in `src/theme/light.ts` and `dark.ts`; dark-mode toggle flows through the store. +- **Client vs server components**: files needing hooks/browser APIs start with `'use client';` (see `LoginForm.tsx`). Pages that only render markup can stay server components. + +## Environment + +Browser-exposed config comes from `NEXT_PUBLIC_*` variables (copy `.env.sample` → `.env`). The ones actually read in code: `NEXT_PUBLIC_ENV`, `NEXT_PUBLIC_DEBUG`, `NEXT_PUBLIC_PUBLIC_URL` (see `src/config.ts`) and `NEXT_PUBLIC_VERSION` (see `src/utils/environment.ts`). `NEXT_PUBLIC_API_URL` is the backend base URL. + +## Gotchas + +- Static export (`output: 'export'`) means **no server-side runtime** — no API routes, no SSR-only features, `images.unoptimized` is on. +- Don't reintroduce the removed demo/showcase route (`src/app/dev`) or `Demo*` components — they were template content. diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..9ae9096 --- /dev/null +++ b/client/README.md @@ -0,0 +1,76 @@ +# Balinyaar — Web Client + +Frontend for the Balinyaar application, built with **Next.js (App Router)**, **React**, **TypeScript**, and **Material UI (MUI)**. + +It ships with a small library of reusable `App*` components, a theming system (light/dark), a lightweight global store, and public/private layouts wired for an authentication flow. + +## Requirements + +- Node.js 18+ and npm +- The backend API (see [`../server`](../server)) running for authenticated features + +## Getting started + +1. Install dependencies: + + ```bash + npm install + ``` + +2. Create your local environment file from the sample and adjust values: + + ```bash + cp .env.sample .env + ``` + + Key variables (all `NEXT_PUBLIC_*` are exposed to the browser): + + | Variable | Purpose | + | ------------------------ | ---------------------------------------------------- | + | `NEXT_PUBLIC_ENV` | `development` \| `preview` \| `production` | + | `NEXT_PUBLIC_DEBUG` | `true` enables extra console logging | + | `NEXT_PUBLIC_PUBLIC_URL` | Public URL of the site | + | `NEXT_PUBLIC_API_URL` | Base URL of the backend API (the `server` project) | + +3. Run the dev server: + + ```bash + npm run dev + ``` + + Open [http://localhost:3000](http://localhost:3000). + +## Available scripts + +| Script | Description | +| ------------------ | ------------------------------------------------------- | +| `npm run dev` | Start the development server with hot reload | +| `npm run build` | Production build (static export to `out/`) | +| `npm run start` | Serve a production build | +| `npm run lint` | Run ESLint (`eslint-config-next`) | +| `npm run format` | Format the codebase with Prettier | +| `npm test` | Run Jest in watch mode | +| `npm run test:ci` | Run Jest once (CI) | +| `npm run type` | Type-check with `tsc` | + +## Project structure + +``` +src/ +├── app/ Next.js App Router routes (home, about, auth, me) + root layout +├── components/ Reusable UI: common App* components (AppButton, AppIcon, ...) + UserInfo +├── hooks/ Custom hooks (auth, layout, events, window size) +├── layout/ PublicLayout / PrivateLayout + TopBar, SideBar, BottomBar +├── store/ Global app store (React context + reducer) +├── theme/ MUI theme provider, light/dark palettes, colors +└── utils/ Helpers (storage, navigation, env, text, types) +``` + +The `@/*` import alias maps to `src/*` (see `tsconfig.json`). + +> For a deeper, agent-oriented map of conventions and where to make changes, see [AGENTS.md](AGENTS.md). + +## Notes + +- `next.config.mjs` uses `output: 'export'`, producing a static site in `out/`. +- Authentication in `src/hooks/auth.ts` and `src/app/auth/login/LoginForm.tsx` is currently a stub (look for `// TODO: AUTH:`); wire it to the backend's JWT endpoints when implementing real auth. diff --git a/client/jest.config.ts b/client/jest.config.ts new file mode 100644 index 0000000..06f47a1 --- /dev/null +++ b/client/jest.config.ts @@ -0,0 +1,27 @@ +import type { Config } from 'jest'; +import nextJest from 'next/jest.js'; + +// const nextJest = require('next/jest'); + +const createJestConfig = nextJest({ + // Provide the path to your Next.js app to load next.config.* and .env files in your test environment + dir: './', +}); + +// Add any custom config to be passed to Jest +const customJestConfig: Config = { + coverageProvider: 'v8', + setupFilesAfterEnv: ['/jest.setup.ts'], + moduleNameMapper: { + // Handle module aliases + '^@/(.*)$': '/$1', + }, + // testEnvironment: 'jest-environment-jsdom', + testEnvironment: 'jsdom', + // transform: { + // '.+\\.(css|styl|less|sass|scss)$': 'jest-css-modules-transform', + // }, +}; + +// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async +module.exports = createJestConfig(customJestConfig); diff --git a/client/jest.setup.ts b/client/jest.setup.ts new file mode 100644 index 0000000..8fb4c36 --- /dev/null +++ b/client/jest.setup.ts @@ -0,0 +1,9 @@ +// Optional: configure or set up a testing framework before each test. +// If you delete this file, remove `setupFilesAfterEnv` from `jest.config.*` + +// Used for __tests__/testing-library.js +// Learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; + +// To get 'next/router' working with tests +jest.mock('next/router', () => require('next-router-mock')); diff --git a/client/next.config.mjs b/client/next.config.mjs new file mode 100644 index 0000000..94c5d1e --- /dev/null +++ b/client/next.config.mjs @@ -0,0 +1,16 @@ +/** @type {import('next').NextConfig} */ + +const nextConfig = { + output: 'export', // Use this if you want to create "static generated website" (SSG), result in "/out" folder + trailingSlash: true, + images: { unoptimized: true }, + + env: { + // Add custom build-time env variables here, also check .env.* files + }, + + reactStrictMode: true, + // reactStrictMode: false, +}; + +export default nextConfig; diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 0000000..f904fda --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,11872 @@ +{ + "name": "nextjs-mui-starter-ts", + "version": "0.2.9", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nextjs-mui-starter-ts", + "version": "0.2.9", + "dependencies": { + "@emotion/cache": "latest", + "@emotion/react": "latest", + "@emotion/server": "latest", + "@emotion/styled": "latest", + "@mui/icons-material": "latest", + "@mui/material": "latest", + "@mui/material-nextjs": "latest", + "clsx": "latest", + "copy-to-clipboard": "latest", + "next": "latest", + "react": "latest", + "react-dom": "latest" + }, + "devDependencies": { + "@testing-library/jest-dom": "latest", + "@testing-library/react": "latest", + "@testing-library/user-event": "latest", + "@types/jest": "latest", + "@types/node": "latest", + "@types/react": "latest", + "@types/react-dom": "latest", + "eslint": "latest", + "eslint-config-next": "latest", + "jest": "latest", + "jest-environment-jsdom": "latest", + "next-router-mock": "latest", + "prettier": "latest", + "ts-node": "latest", + "typescript": "^5" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/server": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/server/-/server-11.11.0.tgz", + "integrity": "sha512-6q89fj2z8VBTx9w93kJ5n51hsmtYuFPtZgnc1L8VzRx9ti4EU6EyvF6Nn1H1x3vcCQCF7u2dB2lY4AYJwUW4PA==", + "license": "MIT", + "dependencies": { + "@emotion/utils": "^1.2.1", + "html-tokenize": "^2.0.0", + "multipipe": "^1.0.2", + "through": "^2.3.8" + }, + "peerDependencies": { + "@emotion/css": "^11.0.0-rc.0" + }, + "peerDependenciesMeta": { + "@emotion/css": { + "optional": true + } + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.11.5", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz", + "integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.2", + "@emotion/serialize": "^1.1.4", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==", + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", + "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.0.tgz", + "integrity": "sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==", + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/console/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/console/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/transform/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/transform/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.15.19", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.19.tgz", + "integrity": "sha512-tCHSi/Tomez9ERynFhZRvFO6n9ATyrPs+2N80DMDzp6xDVirbBjEwhPcE+x7Lj+nwYw0SqFkOxyvMP0irnm55w==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.15.19", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.19.tgz", + "integrity": "sha512-RsEiRxA5azN9b8gI7JRqekkgvxQUlitoBOtZglflb8cUDyP12/cP4gRwhb44Ea1/zwwGGjAj66ZJpGHhKfibNA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.15.19", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.19.tgz", + "integrity": "sha512-lp5xQBbcRuxNtjpWU0BWZgIrv2XLUz4RJ0RqFXBdESIsKoGCQZ6P3wwU5ZPuj5TjssNiKv9AlM+vHopRxZhvVQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.40", + "@mui/core-downloads-tracker": "^5.15.19", + "@mui/system": "^5.15.15", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material-nextjs": { + "version": "5.15.11", + "resolved": "https://registry.npmjs.org/@mui/material-nextjs/-/material-nextjs-5.15.11.tgz", + "integrity": "sha512-cp5RWYbBngyi7NKP91R9QITllfxumCVPFjqe4AKzNROVuCot0VpgkafxXqfbv0uFsyUU0ROs0O2M3r17q604Aw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/cache": "^11.11.0", + "@emotion/server": "^11.11.0", + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "next": "^13.0.0 || ^14.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/cache": { + "optional": true + }, + "@emotion/server": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz", + "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.15.14", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz", + "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.15.tgz", + "integrity": "sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.15.14", + "@mui/styled-engine": "^5.15.14", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.14", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", + "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz", + "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@types/prop-types": "^15.7.11", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@next/env": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", + "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.3.tgz", + "integrity": "sha512-L3oDricIIjgj1AVnRdRor21gI7mShlSwU/1ZGHmqM3LzHhXXhdkrfeNY5zif25Bi5Dd7fiJHsbhoZCHfXYvlAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "10.3.10" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz", + "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", + "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", + "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", + "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", + "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", + "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", + "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", + "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", + "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", + "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.1.0.tgz", + "integrity": "sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.6.tgz", + "integrity": "sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/bun": "latest", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/bun": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.0.tgz", + "integrity": "sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.toreversed": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", + "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", + "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/babel-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz", + "integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001632", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001632.tgz", + "integrity": "sha512-udx3o7yHJfUxMLkGohMlVHCvFvWmirKh9JAH/d7WOLPetlH+LTL5cocMZ0t7oZx/mdlOWXti97xLZWc8uURRHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/create-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/create-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.798", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.798.tgz", + "integrity": "sha512-by9J2CiM9KPGj9qfp5U4FcPSbXJG7FNzqnYaY4WLzX+v2PHieVGmnsA4dxfpGE3QEC7JofpPZmn7Vn1B9NR2+Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", + "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.3.tgz", + "integrity": "sha512-ZkNztm3Q7hjqvB1rRlOX8P9E/cXRL9ajRcs8jufEtwMfTVYRqnmtnaSu57QqHyBlovMuiB8LEzfLBkh5RYV6Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "14.2.3", + "@rushstack/eslint-patch": "^1.3.3", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", + "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "fast-glob": "^3.3.1", + "get-tsconfig": "^4.5.0", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", + "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", + "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "aria-query": "^5.3.0", + "array-includes": "^3.1.7", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "=4.7.0", + "axobject-query": "^3.2.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "es-iterator-helpers": "^1.0.15", + "hasown": "^2.0.0", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.entries": "^1.1.7", + "object.fromentries": "^2.0.7" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.34.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz", + "integrity": "sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.toreversed": "^1.1.2", + "array.prototype.tosorted": "^1.1.3", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.hasown": "^1.1.4", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", + "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-tokenize": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-tokenize/-/html-tokenize-2.0.1.tgz", + "integrity": "sha512-QY6S+hZ0f5m1WT8WffYN+Hg+xm/w5I8XeUcAq/ZYP5wVC8xbKi4Whhru3FtrAebD5EhBW8rmFzkDI6eCAuFe2w==", + "license": "MIT", + "dependencies": { + "buffer-from": "~0.1.1", + "inherits": "~2.0.1", + "minimist": "~1.2.5", + "readable-stream": "~1.0.27-1", + "through2": "~0.4.1" + }, + "bin": { + "html-tokenize": "bin/cmd.js" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-circus/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-circus/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-config/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-each/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-resolve/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-resolve/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runner/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runtime/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-snapshot/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-validate/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-watcher/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-watcher/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/multipipe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-1.0.2.tgz", + "integrity": "sha512-6uiC9OvY71vzSGX8lZvSqscE7ft9nPupJ8fMjrCNRAUy2LREUW42UL+V/NTrogr6rFgRydUrCX4ZitfpSNkSCQ==", + "license": "MIT", + "dependencies": { + "duplexer2": "^0.1.2", + "object-assign": "^4.1.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz", + "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.3", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.3", + "@next/swc-darwin-x64": "14.2.3", + "@next/swc-linux-arm64-gnu": "14.2.3", + "@next/swc-linux-arm64-musl": "14.2.3", + "@next/swc-linux-x64-gnu": "14.2.3", + "@next/swc-linux-x64-musl": "14.2.3", + "@next/swc-win32-arm64-msvc": "14.2.3", + "@next/swc-win32-ia32-msvc": "14.2.3", + "@next/swc-win32-x64-msvc": "14.2.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-router-mock": { + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/next-router-mock/-/next-router-mock-0.9.13.tgz", + "integrity": "sha512-906n2RRaE6Y28PfYJbaz5XZeJ6Tw8Xz1S6E31GGwZ0sXB6/XjldD1/2azn1ZmBmRk5PQRkzjg+n+RHZe5xQzWA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "next": ">=10.0.0", + "react": ">=17.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", + "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.hasown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", + "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/through2": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha512-45Llu+EwHKtAZYTPPVn3XZHBgakWMN3rokhEv5hu596XP+cNgplMg+Gj+1nmAvj+L0K7+N49zBKx5rah5u0QIQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~1.0.17", + "xtend": "~2.1.1" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==", + "dependencies": { + "object-keys": "~0.4.0" + }, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/xtend/node_modules/object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==", + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..a6744c9 --- /dev/null +++ b/client/package.json @@ -0,0 +1,47 @@ +{ + "name": "balinyaar-client", + "version": "0.1.0", + "description": "Balinyaar web application", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "format": "prettier ./ --write", + "lint": "next lint", + "start": "next start", + "test": "jest --watch", + "test:ci": "jest --ci", + "type": "tsc" + }, + "dependencies": { + "@emotion/cache": "latest", + "@emotion/react": "latest", + "@emotion/server": "latest", + "@emotion/styled": "latest", + "@mui/icons-material": "latest", + "@mui/material": "latest", + "@mui/material-nextjs": "latest", + "clsx": "latest", + "copy-to-clipboard": "latest", + "next": "latest", + "react": "latest", + "react-dom": "latest" + }, + "devDependencies": { + "@testing-library/jest-dom": "latest", + "@testing-library/react": "latest", + "@testing-library/user-event": "latest", + "@types/jest": "latest", + "@types/node": "latest", + "@types/react": "latest", + "@types/react-dom": "latest", + "eslint": "latest", + "eslint-config-next": "latest", + "jest": "latest", + "jest-environment-jsdom": "latest", + "next-router-mock": "latest", + "prettier": "latest", + "ts-node": "latest", + "typescript": "^5" + } +} diff --git a/client/public/img/favicon/16x16.png b/client/public/img/favicon/16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..2ece9a6a9564e789cb40d0ea9a6e9b16d556ab92 GIT binary patch literal 479 zcmV<50U-W~P)Px$nMp)JR5(wK(z{E7Q5XjB=QYB>8cdO_77a3jik3(S${`5SAd;enF0=$)Xlg5j z{(!I{Xpm?T5+X>0pdo9hHF8m-MNw1pg85lseJ4IiyLmg~Iq&m3?|C@{0L)|(h260o z5s?l=D+;UmTG+2*%^=J47y-!dd=Xf0JiAo`g)e2Z)p%dL+KnUUq$iwEeF6ge VyXz&!Z`%L>002ovPDHLkV1h5X%rgK0 literal 0 HcmV?d00001 diff --git a/client/public/img/favicon/180x180.png b/client/public/img/favicon/180x180.png new file mode 100644 index 0000000000000000000000000000000000000000..d12ce7402dbc35f853cdd1bafa952803855b34d8 GIT binary patch literal 9127 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 literal 0 HcmV?d00001 diff --git a/client/public/img/favicon/512x512.png b/client/public/img/favicon/512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..8ba6699adb366e00b608d342c161254ab583802d GIT binary patch literal 32040 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 Jm#Nx?{y%+H%#{ED literal 0 HcmV?d00001 diff --git a/client/public/img/logo.svg b/client/public/img/logo.svg new file mode 100644 index 0000000..362252c --- /dev/null +++ b/client/public/img/logo.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/client/public/robots.txt b/client/public/robots.txt new file mode 100644 index 0000000..874bf1f --- /dev/null +++ b/client/public/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Disallow: /private/ + +User-agent: * +Allow: / diff --git a/client/public/site.webmanifest b/client/public/site.webmanifest new file mode 100644 index 0000000..c68685e --- /dev/null +++ b/client/public/site.webmanifest @@ -0,0 +1,22 @@ +{ + "name": "Balinyaar", + "short_name": "Balinyaar", + "description": "Balinyaar web application", + "start_url": ".", + "theme_color": "#000000", + "background_color": "#ffffff", + "display": "standalone", + "orientation": "portrait", + "icons": [ + { + "src": "favicon.ico?v=1.0", + "sizes": "48x48 32x32 16x16", + "type": "image/x-icon" + }, + { "src": "img/favicon/16x16.png?v=1.0", "sizes": "16x16", "type": "image/png" }, + { "src": "img/favicon/32x32.png?v=1.0", "sizes": "32x32", "type": "image/png" }, + { "src": "img/favicon/180x180.png?v=1.0", "sizes": "180x180", "type": "image/png" }, + { "src": "img/favicon/192x192.png?v=1.0", "sizes": "192x192", "type": "image/png" }, + { "src": "img/favicon/512x512.png?v=1.0", "sizes": "512x512", "type": "image/png" } + ] +} diff --git a/client/src/app/about/page.tsx b/client/src/app/about/page.tsx new file mode 100644 index 0000000..bff88f8 --- /dev/null +++ b/client/src/app/about/page.tsx @@ -0,0 +1,19 @@ +import { Stack, Typography } from '@mui/material'; +import { NextPage } from 'next'; + +/** + * Renders About Application page + * @page About + */ +const AboutPage: NextPage = () => { + return ( + + + About application + Balinyaar is a Next.js (App Router) application built with Material UI. + + + ); +}; + +export default AboutPage; diff --git a/client/src/app/auth/login/LoginForm.tsx b/client/src/app/auth/login/LoginForm.tsx new file mode 100644 index 0000000..74d50de --- /dev/null +++ b/client/src/app/auth/login/LoginForm.tsx @@ -0,0 +1,42 @@ +'use client'; +import { Stack } from '@mui/material'; +import { useRouter } from 'next/navigation'; +import { AppButton } from '@/components'; +import { useAppStore } from '@/store'; +import { useEventLogout } from '@/hooks'; +import { sessionStorageSet } from '@/utils'; + +/** + * Renders login form for user to authenticate + * @component LoginForm + */ +const LoginForm = () => { + const router = useRouter(); + const [, dispatch] = useAppStore(); + const onLogout = useEventLogout(); + + const onLogin = () => { + // TODO: AUTH: Sample of access token store, replace next line in real application + sessionStorageSet('access_token', 'TODO:_save-real-access-token-here'); + + dispatch({ type: 'LOG_IN' }); + router.replace('/'); // Redirect to home page without ability to go back + }; + + return ( + + Put form controls or add social login buttons here... + + + + Emulate User Login + + + Logout User + + + + ); +}; + +export default LoginForm; diff --git a/client/src/app/auth/login/page.tsx b/client/src/app/auth/login/page.tsx new file mode 100644 index 0000000..5fc2c8d --- /dev/null +++ b/client/src/app/auth/login/page.tsx @@ -0,0 +1,21 @@ +import { Metadata, NextPage } from 'next'; +import LoginForm from './LoginForm'; + +/** + * User Login page + * @page Login + */ +const LoginPage: NextPage = () => { + return ( + <> + + + ); +}; + +export const metadata: Metadata = { + title: 'Login - Balinyaar', + description: 'Balinyaar web application', +}; + +export default LoginPage; diff --git a/client/src/app/auth/page.tsx b/client/src/app/auth/page.tsx new file mode 100644 index 0000000..7cd4984 --- /dev/null +++ b/client/src/app/auth/page.tsx @@ -0,0 +1,13 @@ +import { redirect } from 'next/navigation'; + +/** + * Redirects to default Auth page + * @page Auth + * @redirect /auth + */ +const AuthPage = () => { + redirect('/auth/login'); + // return

Auth Page
; +}; + +export default AuthPage; diff --git a/client/src/app/auth/signup/page.tsx b/client/src/app/auth/signup/page.tsx new file mode 100644 index 0000000..c9eb959 --- /dev/null +++ b/client/src/app/auth/signup/page.tsx @@ -0,0 +1,9 @@ +import { Metadata } from 'next'; +import LoginPage from '../login/page'; + +export const metadata: Metadata = { + title: 'Signup - Balinyaar', + description: 'Balinyaar web application', +}; + +export default LoginPage; // Reuses the Login page for now diff --git a/client/src/app/favicon.ico b/client/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a298d56899aaa0db653915d3d18410d1909ffd1a GIT binary patch literal 15406 zcmeHOeQ*=U6+bs=N*J8dObeOxk22E%88@*3lgtFtLT6h3GLVpQW0?s!q~l3rTZE7V znr2e@gBCm#5(pE!w(LM0Q)~=TJpRI>7;Ix~>%+1op|eTI#}q7ybV}j`3y`+&owVY7 zmQH7%PIjl<%+B7O?(O@%x4(V+Zui763Cumr+_?rqeVKWs!->AudtH! zKp}bBW8}AM*1sb~KE0c)&ThLtS^#n=8!8pfmC@u;Hr+@@*BS}g+;*LW+R(9{46sE^ z9%Vs}lSTP)ZRprT(PXJ!Dobm$rKr@gFp@kUGnJt=*JdwSH5FNCV-(vKlf`GKE`E)A z#bnz~R;_NZUi^B4$>-&7lHs_W6D=jVU`?1u&ri;wm*#7O{ zpWh$8GZ*mZh4JU*vDTg5M+Pyjsn32#qG13ZJsm6kXgAeOW2`6!9Xs~A&Y2hs{o!;= zz|a&c-D`tBr#gjoI6uH#zgez+F}tH+_)1%5oGnNHWOP$3AwTHYUGdgUF=U;+F`zv# z{J>Q8PsMJk8}fw#l%E#ez|sFl-8eSo+8u3t)BY9K7!T#@7RFSej!4&Xbzib}%`@5t ztXoWDwK?psVSZz6xud7OfdB^5aY{M{xw>C{Ze*UrY_glXT1jJV8Ht2p zi}6P2{fGKquI_Ca*B{hmx&}-B_yuuVO=JRw34OuigUm68u^8S>-o|$vEFlc%Mf?Dq z#Bm|lZaNQWvOET=e;fvpzQD05C$>=CSm&!<1$_M+hMmE_5c&c7qNTpVhVg;lhMe>4 zY%)ue3?UmW!hqeiR7{|$S2gZkR^0BzLE#u4kBVllu z&323rvCt#d24c{tU8e+ouwr}=lZ)kV+1Zz8K|Cmgf}D68Fg+N{_V2be-UGN!0!y?^ zHk6sPP6BYIdL*n=sS=l`s8newi+`V%s@lCgW$`0ZZ7g1L`3Hu7psjE`@^bOX=N?=3 zXf>?Qxam60($u8C#tX-!^bnGuJ$!Kytns`Kr5)B>zSyz9{hO65R{AM+te2)HZ<$oP z3O!5^FRTw09T{3&q+_?ky3hqExA^NrZmytvOqdS*88$LU-SOStoi(5NBHxiS@rhsS*jJ_lJNUh1 z9r&GMr#0FjxgT`wtvBBv{CPa_^EpE?3u~!dDySUlW*8E;af_S@@;$^EA$EE#g-b3TGnO zgk0Kj^z+-0XXGl0Jr&4is zB;O7C^7$QYb%!q5dwsZPS!}s>Q-C2HE-CjE=S-(wpV)F-_g+(7xwp$uPlj#1#BR6v z_n2L`WZHn(6^%2Uo%%$!zi*$V`;@QsL?vme`#b4AqX!#$h}~iF?=oMz{glx!#m~ZL z6pu5wk9F$A2c>q?IUnx#f(-<1z^viO*Pzp z_vwae;;?jZHqd_Ha|vxIHhX+gxTH8|UVM6F-pg68caK#J`EaisY=G~BkA-Z&eh9}U z9Mi}pv>_aqxST*?&gA4=nGO5A`(S_3yM6NrIaa}vp+O^It=%CTCU7Y@rjbiH9yl&p z#>b_20418kc;{ z^%bT1nsUXzWtiv-lJJKNj|%=5VdCTFp9i=!#Et1NaS38>-+s)OCbk{FJKHv0`zc^H zLW$T9fw*Mk<_DbPnqZ!8tv^!M3G;URkVe*&`>baX;K%PwBHDoSLvW7s8}uKRDe>J; zWQ5UvAqi+hexM(?Z^yuvadnVOJ_qI+GsN#gtMW2E4+m`s(+@5${AcIjOOP)Kl!6Ic5H literal 0 HcmV?d00001 diff --git a/client/src/app/globals.css b/client/src/app/globals.css new file mode 100644 index 0000000..55ce595 --- /dev/null +++ b/client/src/app/globals.css @@ -0,0 +1,18 @@ +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +html, +body { + max-width: 100vw; + overflow-x: hidden; + /* required for sticky elements: HeaderMobile, and so on */ + max-height: 100vh; +} + +a { + color: inherit; + text-decoration: none; +} diff --git a/client/src/app/home/page.tsx b/client/src/app/home/page.tsx new file mode 100644 index 0000000..0a3bdb8 --- /dev/null +++ b/client/src/app/home/page.tsx @@ -0,0 +1,24 @@ +import { Metadata, NextPage } from 'next'; +import { Stack, Typography } from '@mui/material'; + +export const metadata: Metadata = { + title: 'Balinyaar', + description: 'Balinyaar web application', +}; + +/** + * Main page of the Application + * @page Home + */ +const Home: NextPage = () => { + return ( + + + Welcome to Balinyaar + This is the home page of the application. + + + ); +}; + +export default Home; diff --git a/client/src/app/layout.tsx b/client/src/app/layout.tsx new file mode 100644 index 0000000..06c5a5c --- /dev/null +++ b/client/src/app/layout.tsx @@ -0,0 +1,35 @@ +import { FunctionComponent, PropsWithChildren } from 'react'; +import { Metadata, Viewport } from 'next'; +import { SimplePaletteColorOptions } from '@mui/material'; +import { AppStoreProvider } from '@/store'; +import defaultTheme, { ThemeProvider } from '@/theme'; +import CurrentLayout from '@/layout'; +import './globals.css'; + +const THEME_COLOR = (defaultTheme.palette?.primary as SimplePaletteColorOptions)?.main || '#FFFFFF'; + +export const viewport: Viewport = { + themeColor: THEME_COLOR, +}; + +export const metadata: Metadata = { + title: 'Balinyaar', + description: 'Balinyaar web application', + manifest: '/site.webmanifest', +}; + +const RootLayout: FunctionComponent = ({ children }) => { + return ( + + + + + {children} + + + + + ); +}; + +export default RootLayout; diff --git a/client/src/app/me/page.tsx b/client/src/app/me/page.tsx new file mode 100644 index 0000000..2155112 --- /dev/null +++ b/client/src/app/me/page.tsx @@ -0,0 +1,18 @@ +import { Stack } from '@mui/material'; +import { NextPage } from 'next'; +import { AppAlert, UserInfo } from '../../components'; + +/** + * Renders User Profile Page + * @page Me + */ +const MeAkaProfilePage: NextPage = () => { + return ( + + This page is under construction + + + ); +}; + +export default MeAkaProfilePage; diff --git a/client/src/app/page.tsx b/client/src/app/page.tsx new file mode 100644 index 0000000..43e21bc --- /dev/null +++ b/client/src/app/page.tsx @@ -0,0 +1,3 @@ +import HomePage from './home/page'; + +export default HomePage; diff --git a/client/src/components/UserInfo/UserInfo.tsx b/client/src/components/UserInfo/UserInfo.tsx new file mode 100644 index 0000000..8ef49e2 --- /dev/null +++ b/client/src/components/UserInfo/UserInfo.tsx @@ -0,0 +1,44 @@ +import { Avatar, Stack, Typography } from '@mui/material'; +import { AppLink } from '../common'; + +interface UserInfoProps { + className?: string; + showAvatar?: boolean; + user?: any; +} + +/** + * Renders User info with Avatar + * @component UserInfo + * @param {boolean} [showAvatar] - user's avatar picture is shown when true + * @param {object} [user] - logged user data {name, email, avatar...} + */ +const UserInfo = ({ showAvatar = false, user, ...restOfProps }: UserInfoProps) => { + const fullName = user?.name || [user?.nameFirst || '', user?.nameLast || ''].join(' ').trim(); + const srcAvatar = user?.avatar ? user?.avatar : undefined; + const userPhoneOrEmail = user?.phone || (user?.email as string); + + return ( + + {showAvatar ? ( + + + + ) : null} + + {fullName || 'Current User'} + + {userPhoneOrEmail || 'Loading...'} + + ); +}; + +export default UserInfo; diff --git a/client/src/components/UserInfo/index.tsx b/client/src/components/UserInfo/index.tsx new file mode 100644 index 0000000..ed0f70b --- /dev/null +++ b/client/src/components/UserInfo/index.tsx @@ -0,0 +1,4 @@ +import UserInfo from './UserInfo'; + +export { UserInfo }; +export default UserInfo; diff --git a/client/src/components/common/AppAlert/AppAlert.test.tsx b/client/src/components/common/AppAlert/AppAlert.test.tsx new file mode 100644 index 0000000..4c3e3af --- /dev/null +++ b/client/src/components/common/AppAlert/AppAlert.test.tsx @@ -0,0 +1,55 @@ +import { render, screen } from '@testing-library/react'; +import AppAlert from './AppAlert'; +import { capitalize, randomText } from '@/utils'; +import { AlertProps } from '@mui/material'; + +const ComponentToTest = AppAlert; + +/** + * Tests for component + */ +describe(' component', () => { + it('renders itself', () => { + const testId = randomText(8); + render(); + const alert = screen.getByTestId(testId); + expect(alert).toBeDefined(); + expect(alert).toHaveAttribute('role', 'alert'); + expect(alert).toHaveClass('MuiAlert-root'); + }); + + it('supports .severity property', () => { + const SEVERITIES = ['error', 'info', 'success', 'warning']; + for (const severity of SEVERITIES) { + const testId = randomText(8); + const severity = 'success'; + render( + + ); + const alert = screen.getByTestId(testId); + expect(alert).toBeDefined(); + expect(alert).toHaveClass(`MuiAlert-filled${capitalize(severity)}`); + } + }); + + it('supports .variant property', () => { + const VARIANTS = ['filled', 'outlined', 'standard']; + for (const variant of VARIANTS) { + const testId = randomText(8); + render( + + ); + const alert = screen.getByTestId(testId); + expect(alert).toBeDefined(); + expect(alert).toHaveClass(`MuiAlert-${variant}Warning`); + } + }); +}); diff --git a/client/src/components/common/AppAlert/AppAlert.tsx b/client/src/components/common/AppAlert/AppAlert.tsx new file mode 100644 index 0000000..d3369f6 --- /dev/null +++ b/client/src/components/common/AppAlert/AppAlert.tsx @@ -0,0 +1,18 @@ +import MuiAlert, { AlertProps as MuiAlertProps } from '@mui/material/Alert'; +import { FunctionComponent } from 'react'; +import { APP_ALERT_SEVERITY, APP_ALERT_VARIANT } from '../../config'; + +/** + * Application styled Alert component + * @component AppAlert + */ +const AppAlert: FunctionComponent = ({ + severity = APP_ALERT_SEVERITY, + variant = APP_ALERT_VARIANT, + onClose, + ...restOfProps +}) => { + return ; +}; + +export default AppAlert; diff --git a/client/src/components/common/AppAlert/index.tsx b/client/src/components/common/AppAlert/index.tsx new file mode 100644 index 0000000..631aecd --- /dev/null +++ b/client/src/components/common/AppAlert/index.tsx @@ -0,0 +1,3 @@ +import AppAlert from './AppAlert'; + +export { AppAlert as default, AppAlert }; diff --git a/client/src/components/common/AppButton/AppButton.test.tsx b/client/src/components/common/AppButton/AppButton.test.tsx new file mode 100644 index 0000000..0653202 --- /dev/null +++ b/client/src/components/common/AppButton/AppButton.test.tsx @@ -0,0 +1,155 @@ +import { FunctionComponent } from 'react'; +import { render, screen, within } from '@testing-library/react'; +import { ThemeProvider } from '../../../theme'; +import AppButton, { AppButtonProps } from './AppButton'; +import DefaultIcon from '@mui/icons-material/MoreHoriz'; +import { randomText, capitalize } from '@/utils'; + +/** + * AppButton wrapped with Theme Provider + */ +const ComponentToTest: FunctionComponent = (props) => ( + + + +); + +/** + * Test specific color for AppButton + * @param {string} colorName - name of the color, one of ColorName type + * @param {string} [expectedClassName] - optional value to be found in className (color "true" may use "success" class name) + * @param {boolean} [ignoreClassName] - optional flag to ignore className (color "inherit" doesn't use any class name) + */ +function testButtonColor(colorName: string, ignoreClassName = false, expectedClassName = colorName) { + it(`supports "${colorName}" color`, () => { + const testId = randomText(8); + let text = `${colorName} button`; + render( + + {text} + + ); + + let button = screen.getByTestId(testId); + expect(button).toBeDefined(); + // console.log('button.className:', button?.className); + if (!ignoreClassName) { + expect(button?.className?.includes('MuiButton-root')).toBeTruthy(); + expect(button?.className?.includes('MuiButton-contained')).toBeTruthy(); + expect(button?.className?.includes(`MuiButton-contained${capitalize(expectedClassName)}`)).toBeTruthy(); // Check for "MuiButton-contained[Primary| Secondary |...]" class + } + }); +} + +describe(' component', () => { + // beforeEach(() => {}); + + it('renders itself', () => { + let text = 'sample button'; + const testId = randomText(8); + render({text}); + const button = screen.getByTestId(testId); + expect(button).toBeDefined(); + expect(button).toHaveAttribute('role', 'button'); + expect(button).toHaveAttribute('type', 'button'); // not "submit" or "input" by default + }); + + it('has .margin style by default', () => { + let text = 'button with default margin'; + const testId = randomText(8); + render({text}); + const button = screen.getByTestId(testId); + expect(button).toBeDefined(); + expect(button).toHaveStyle('margin: 8px'); // Actually it is theme.spacing(1) value + }); + + it('supports .className property', () => { + let text = 'button with specific class'; + let className = 'someClassName'; + const testId = randomText(8); + render( + + {text} + + ); + const button = screen.getByTestId(testId); + expect(button).toBeDefined(); + expect(button).toHaveClass(className); + }); + + it('supports .label property', () => { + let text = 'button with label'; + render(); + let span = screen.getByText(text); + expect(span).toBeDefined(); + let button = span.closest('button'); // parent + ); +}; + +export default AppButton; diff --git a/client/src/components/common/AppButton/index.tsx b/client/src/components/common/AppButton/index.tsx new file mode 100644 index 0000000..9eea0cc --- /dev/null +++ b/client/src/components/common/AppButton/index.tsx @@ -0,0 +1,3 @@ +import AppButton from './AppButton'; + +export { AppButton as default, AppButton }; diff --git a/client/src/components/common/AppIcon/AppIcon.test.tsx b/client/src/components/common/AppIcon/AppIcon.test.tsx new file mode 100644 index 0000000..bfc5e02 --- /dev/null +++ b/client/src/components/common/AppIcon/AppIcon.test.tsx @@ -0,0 +1,64 @@ +import { render, screen } from '@testing-library/react'; +import AppIcon from './AppIcon'; +import { APP_ICON_SIZE } from '../../config'; +import { randomColor, randomText } from '@/utils'; +import { ICONS } from './config'; + +const ComponentToTest = AppIcon; + +/** + * Tests for component + */ +describe(' component', () => { + it('renders itself', () => { + const testId = randomText(8); + render(); + const svg = screen.getByTestId(testId); + expect(svg).toBeDefined(); + expect(svg).toHaveAttribute('data-icon', 'default'); + expect(svg).toHaveAttribute('size', String(APP_ICON_SIZE)); // default size + expect(svg).toHaveAttribute('height', String(APP_ICON_SIZE)); // default size when .size is not set + expect(svg).toHaveAttribute('width', String(APP_ICON_SIZE)); // default size when .size is not se + }); + + it('supports .color property', () => { + const testId = randomText(8); + const color = randomColor(); // Note: 'rgb(255, 128, 0)' format is used by react-icons npm, so tests may fail + render(); + const svg = screen.getByTestId(testId); + expect(svg).toHaveAttribute('data-icon', 'default'); + // expect(svg).toHaveAttribute('color', color); // TODO: Looks like MUI Icons exclude .color property from rendering + expect(svg).toHaveStyle(`color: ${color}`); + expect(svg).toHaveAttribute('fill', 'currentColor'); // .fill must be 'currentColor' when .color property is set + }); + + it('supports .icon property', () => { + // Verify that all icons are supported + for (const icon of Object.keys(ICONS)) { + const testId = randomText(8); + render(); + const svg = screen.getByTestId(testId); + expect(svg).toBeDefined(); + expect(svg).toHaveAttribute('data-icon', icon.toLowerCase()); + } + }); + + it('supports .size property', () => { + const testId = randomText(8); + const size = Math.floor(Math.random() * 128) + 1; + render(); + const svg = screen.getByTestId(testId); + expect(svg).toHaveAttribute('size', String(size)); + expect(svg).toHaveAttribute('height', String(size)); + expect(svg).toHaveAttribute('width', String(size)); + }); + + it('supports .title property', () => { + const testId = randomText(8); + const title = randomText(16); + render(); + const svg = screen.getByTestId(testId); + expect(svg).toBeDefined(); + expect(svg).toHaveAttribute('title', title); + }); +}); diff --git a/client/src/components/common/AppIcon/AppIcon.tsx b/client/src/components/common/AppIcon/AppIcon.tsx new file mode 100644 index 0000000..4c19dba --- /dev/null +++ b/client/src/components/common/AppIcon/AppIcon.tsx @@ -0,0 +1,51 @@ +import { ComponentType, FunctionComponent, SVGAttributes } from 'react'; +import { APP_ICON_SIZE } from '../../config'; +import { IconName, ICONS } from './config'; + +/** + * Props of the AppIcon component, also can be used for SVG icons + */ +export interface Props extends SVGAttributes { + color?: string; + icon?: IconName | string; + size?: string | number; + title?: string; +} + +/** + * Renders SVG icon by given Icon name + * @component AppIcon + * @param {string} [color] - color of the icon as a CSS color value + * @param {string} [icon] - name of the Icon to render + * @param {string} [title] - title/hint to show when the cursor hovers the icon + * @param {string | number} [size] - size of the icon, default is ICON_SIZE + */ +const AppIcon: FunctionComponent = ({ + color, + icon = 'default', + size = APP_ICON_SIZE, + style, + ...restOfProps +}) => { + const iconName = (icon || 'default').trim().toLowerCase() as IconName; + + let ComponentToRender: ComponentType = ICONS[iconName]; + if (!ComponentToRender) { + console.warn(`AppIcon: icon "${iconName}" is not found!`); + ComponentToRender = ICONS.default; // ICONS['default']; + } + + const propsToRender = { + height: size, + color, + fill: color && 'currentColor', + size, + style: { ...style, color }, + width: size, + ...restOfProps, + }; + + return ; +}; + +export default AppIcon; diff --git a/client/src/components/common/AppIcon/config.ts b/client/src/components/common/AppIcon/config.ts new file mode 100644 index 0000000..d625175 --- /dev/null +++ b/client/src/components/common/AppIcon/config.ts @@ -0,0 +1,56 @@ +// SVG assets +import PencilIcon from './icons/PencilIcon'; +// MUI Icons +import DefaultIcon from '@mui/icons-material/MoreHoriz'; +import SettingsIcon from '@mui/icons-material/Settings'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; +import MenuIcon from '@mui/icons-material/Menu'; +import CloseIcon from '@mui/icons-material/Close'; +import DayNightIcon from '@mui/icons-material/Brightness4'; +import NightIcon from '@mui/icons-material/Brightness3'; +import DayIcon from '@mui/icons-material/Brightness5'; +import SearchIcon from '@mui/icons-material/Search'; +import InfoIcon from '@mui/icons-material/Info'; +import HomeIcon from '@mui/icons-material/Home'; +import AccountCircle from '@mui/icons-material/AccountCircle'; +import PersonAddIcon from '@mui/icons-material/PersonAdd'; +import PersonIcon from '@mui/icons-material/Person'; +import ExitToAppIcon from '@mui/icons-material/ExitToApp'; +import NotificationsIcon from '@mui/icons-material/NotificationsOutlined'; +import DangerousIcon from '@mui/icons-material/Dangerous'; + +/** + * List of all available Icon names + */ +export type IconName = keyof typeof ICONS; + +/** + * How to use: + * 1. Import all required React, MUI or other SVG icons into this file. + * 2. Add icons with "unique lowercase names" into ICONS object. Lowercase is a must! + * 3. Use icons everywhere in the App by their names in component + * Important: properties of ICONS object MUST be lowercase! + * Note: You can use camelCase or UPPERCASE in the component + */ +export const ICONS /* Note: Setting type disables property autocomplete :( was - : Record */ = { + default: DefaultIcon, + logo: PencilIcon, + close: CloseIcon, + menu: MenuIcon, + settings: SettingsIcon, + visibilityon: VisibilityIcon, + visibilityoff: VisibilityOffIcon, + daynight: DayNightIcon, + night: NightIcon, + day: DayIcon, + search: SearchIcon, + info: InfoIcon, + home: HomeIcon, + account: AccountCircle, + signup: PersonAddIcon, + login: PersonIcon, + logout: ExitToAppIcon, + notifications: NotificationsIcon, + error: DangerousIcon, +}; diff --git a/client/src/components/common/AppIcon/icons/CurrencyIcon.tsx b/client/src/components/common/AppIcon/icons/CurrencyIcon.tsx new file mode 100644 index 0000000..cd55be4 --- /dev/null +++ b/client/src/components/common/AppIcon/icons/CurrencyIcon.tsx @@ -0,0 +1,29 @@ +import { FunctionComponent } from 'react'; +import { IconProps } from '../utils'; + +const CurrencyIcon: FunctionComponent = (props) => { + return ( + + + + + + + + + ); +}; + +export default CurrencyIcon; diff --git a/client/src/components/common/AppIcon/icons/PencilIcon.tsx b/client/src/components/common/AppIcon/icons/PencilIcon.tsx new file mode 100644 index 0000000..d992599 --- /dev/null +++ b/client/src/components/common/AppIcon/icons/PencilIcon.tsx @@ -0,0 +1,29 @@ +import { FunctionComponent } from 'react'; +import { IconProps } from '../utils'; + +const PencilIcon: FunctionComponent = (props) => { + return ( + + + + + + + + + ); +}; + +export default PencilIcon; diff --git a/client/src/components/common/AppIcon/icons/YellowPlanIcon.tsx b/client/src/components/common/AppIcon/icons/YellowPlanIcon.tsx new file mode 100644 index 0000000..11cebc6 --- /dev/null +++ b/client/src/components/common/AppIcon/icons/YellowPlanIcon.tsx @@ -0,0 +1,125 @@ +import { FunctionComponent } from 'react'; +import { IconProps } from '../utils'; + +const YellowPlaneIcon: FunctionComponent = (props) => { + const styleOpacityAndEnableBackground = { + opacity: 0.2, + // enableBackground: 'new' + }; + + return ( + + + + + + + + + + + + + + + + + + + ); +}; + +export default YellowPlaneIcon; diff --git a/client/src/components/common/AppIcon/index.tsx b/client/src/components/common/AppIcon/index.tsx new file mode 100644 index 0000000..eef6266 --- /dev/null +++ b/client/src/components/common/AppIcon/index.tsx @@ -0,0 +1,3 @@ +import AppIcon from './AppIcon'; + +export { AppIcon as default, AppIcon }; diff --git a/client/src/components/common/AppIcon/utils.ts b/client/src/components/common/AppIcon/utils.ts new file mode 100644 index 0000000..d914021 --- /dev/null +++ b/client/src/components/common/AppIcon/utils.ts @@ -0,0 +1,11 @@ +import { SVGAttributes } from 'react'; + +/** + * Props to use with custom SVG icons, similar to AppIcon's Props + */ +export interface IconProps extends SVGAttributes { + color?: string; + icon?: string; + size?: string | number; + title?: string; +} diff --git a/client/src/components/common/AppIconButton/AppIconButton.test.tsx b/client/src/components/common/AppIconButton/AppIconButton.test.tsx new file mode 100644 index 0000000..b8b6de5 --- /dev/null +++ b/client/src/components/common/AppIconButton/AppIconButton.test.tsx @@ -0,0 +1,126 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import AppIconButton, { MUI_ICON_BUTTON_COLORS } from './AppIconButton'; +import { APP_ICON_SIZE } from '../../config'; +import { capitalize, randomColor, randomText } from '@/utils'; +import { ICONS } from '../AppIcon/config'; + +const ComponentToTest = AppIconButton; + +function randomPropertyName(obj: object): string { + const objectProperties = Object.keys(obj); + const propertyName = objectProperties[Math.floor(Math.random() * objectProperties.length)]; + return propertyName; +} + +// function randomPropertyValue(obj: object): unknown { +// const propertyName = randomPropertyName(obj); +// return (obj as ObjectPropByName)[propertyName]; +// } + +/** + * Tests for component + */ +describe(' component', () => { + it('renders itself', () => { + const testId = randomText(8); + render(); + + // Button + const button = screen.getByTestId(testId); + expect(button).toBeDefined(); + expect(button).toHaveAttribute('role', 'button'); + expect(button).toHaveAttribute('type', 'button'); + + // Icon + const svg = button.querySelector('svg'); + expect(svg).toBeDefined(); + expect(svg).toHaveAttribute('data-icon', 'default'); // default icon + expect(svg).toHaveAttribute('size', String(APP_ICON_SIZE)); // default size + expect(svg).toHaveAttribute('height', String(APP_ICON_SIZE)); // default size when .size is not set + expect(svg).toHaveAttribute('width', String(APP_ICON_SIZE)); // default size when .size is not se + }); + + it('supports .color property', () => { + for (const color of [...MUI_ICON_BUTTON_COLORS, randomColor(), randomColor(), randomColor()]) { + const testId = randomText(8); + const icon = randomPropertyName(ICONS) as string; + render(); + + // Button + const button = screen.getByTestId(testId); + expect(button).toBeDefined(); + + if (color == 'default') { + return; // Nothing to test for default color + } + + if (MUI_ICON_BUTTON_COLORS.includes(color)) { + expect(button).toHaveClass(`MuiIconButton-color${capitalize(color)}`); + } else { + expect(button).toHaveStyle({ color: color }); + } + } + }); + + it('supports .disable property', () => { + const testId = randomText(8); + const title = randomText(16); + render(); + + // Button + const button = screen.getByTestId(testId); + expect(button).toBeDefined(); + expect(button).toHaveAttribute('aria-disabled', 'true'); + expect(button).toHaveClass('Mui-disabled'); + }); + + it('supports .icon property', () => { + // Verify that all icons are supported + for (const icon of Object.keys(ICONS)) { + const testId = randomText(8); + render(); + + // Button + const button = screen.getByTestId(testId); + expect(button).toBeDefined(); + + // Icon + const svg = button.querySelector('svg'); + expect(button).toBeDefined(); + expect(svg).toHaveAttribute('data-icon', icon.toLowerCase()); + } + }); + + it('supports .size property', () => { + const sizes = ['small', 'medium', 'large'] as const; // as IconButtonProps['size'][]; + for (const size of sizes) { + const testId = randomText(8); + render(); + + // Button + const button = screen.getByTestId(testId); + expect(button).toBeDefined(); + expect(button).toHaveClass(`MuiIconButton-size${capitalize(size)}`); // MuiIconButton-sizeSmall | MuiIconButton-sizeMedium | MuiIconButton-sizeLarge + } + }); + + it('supports .title property', async () => { + const testId = randomText(8); + const title = randomText(16); + render(); + + // Button + const button = screen.getByTestId(testId); + expect(button).toBeDefined(); + expect(button).toHaveAttribute('aria-label', title); + + // Emulate mouseover event to show tooltip + await fireEvent(button, new MouseEvent('mouseover', { bubbles: true })); + + // Tooltip is rendered in a separate div, so we need to find it by role + const tooltip = await screen.findByRole('tooltip'); + expect(tooltip).toBeDefined(); + expect(tooltip).toHaveTextContent(title); + expect(tooltip).toHaveClass('MuiTooltip-popper'); + }); +}); diff --git a/client/src/components/common/AppIconButton/AppIconButton.tsx b/client/src/components/common/AppIconButton/AppIconButton.tsx new file mode 100644 index 0000000..a2d0257 --- /dev/null +++ b/client/src/components/common/AppIconButton/AppIconButton.tsx @@ -0,0 +1,98 @@ +import { ElementType, FunctionComponent, useMemo } from 'react'; +import { Tooltip, IconButton, IconButtonProps, TooltipProps } from '@mui/material'; +import AppIcon from '../AppIcon'; +import AppLink from '../AppLink'; +import { alpha } from '@mui/material'; +import { Props } from '../AppIcon/AppIcon'; +import { IconName } from '../AppIcon/config'; + +export const MUI_ICON_BUTTON_COLORS = [ + 'inherit', + 'default', + 'primary', + 'secondary', + 'success', + 'error', + 'info', + 'warning', +]; + +export interface AppIconButtonProps extends Omit { + color?: string; // Not only 'inherit' | 'default' | 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning', + icon?: IconName | string; + iconProps?: Partial; + // Missing props + component?: ElementType; // Could be RouterLink, AppLink, , etc. + to?: string; // Link prop + href?: string; // Link prop + openInNewTab?: boolean; // Link prop + tooltipProps?: Partial; +} + +/** + * Renders MUI IconButton with SVG image by given Icon name + * @param {string} [color] - color of background and hover effect. Non MUI values is also accepted. + * @param {boolean} [disabled] - the IconButton is not active when true, also the Tooltip is not rendered. + * @param {string} [href] - external link URI + * @param {string} [icon] - name of Icon to render inside the IconButton + * @param {object} [iconProps] - additional props to pass into the AppIcon component + * @param {boolean} [openInNewTab] - link will be opened in new tab when true + * @param {string} [size] - size of the button: 'small', 'medium' or 'large' + * @param {Array | func | object} [sx] - additional CSS styles to apply to the button + * @param {string} [title] - when set, the IconButton is rendered inside Tooltip with this text + * @param {string} [to] - internal link URI + * @param {object} [tooltipProps] - additional props to pass into the Tooltip component + */ +const AppIconButton: FunctionComponent = ({ + color = 'default', + component, + children, + disabled, + icon, + iconProps, + sx, + title, + tooltipProps, + ...restOfProps +}) => { + const componentToRender = !component && (restOfProps?.href || restOfProps?.to) ? AppLink : component ?? IconButton; + + const isMuiColor = useMemo(() => MUI_ICON_BUTTON_COLORS.includes(color), [color]); + + const iconButtonToRender = useMemo(() => { + const colorToRender = isMuiColor ? (color as IconButtonProps['color']) : 'default'; + const sxToRender = { + ...sx, + ...(!isMuiColor && { + color: color, + ':hover': { + backgroundColor: alpha(color, 0.04), + }, + }), + }; + return ( + + + {children} + + ); + }, [color, componentToRender, children, disabled, icon, isMuiColor, sx, iconProps, restOfProps]); + + // When title is set, wrap the IconButton with Tooltip. + // Note: when IconButton is disabled the Tooltip is not working, so we don't need it + return title && !disabled ? ( + + {iconButtonToRender} + + ) : ( + iconButtonToRender + ); +}; + +export default AppIconButton; diff --git a/client/src/components/common/AppIconButton/index.tsx b/client/src/components/common/AppIconButton/index.tsx new file mode 100644 index 0000000..892fa04 --- /dev/null +++ b/client/src/components/common/AppIconButton/index.tsx @@ -0,0 +1,3 @@ +import AppIconButton from './AppIconButton'; + +export { AppIconButton as default, AppIconButton }; diff --git a/client/src/components/common/AppImage/AppImage.test.tsx b/client/src/components/common/AppImage/AppImage.test.tsx new file mode 100644 index 0000000..afc7539 --- /dev/null +++ b/client/src/components/common/AppImage/AppImage.test.tsx @@ -0,0 +1,55 @@ +import { render, screen } from '@testing-library/react'; +import { randomText } from '@/utils'; +import AppImage from './AppImage'; + +const ComponentToTest = AppImage; + +/** + * Tests for component + */ +describe(' component', () => { + const src = 'https:/domain.com/image.jpg'; + + it('renders itself', () => { + const testId = randomText(8); + render(); + const image = screen.getByTestId(testId); + expect(image).toBeDefined(); + expect(image).toHaveAttribute('src', src); + expect(image).toHaveAttribute('alt', 'Image'); // Default prop value + expect(image).toHaveAttribute('height', '256'); // Default prop value + expect(image).toHaveAttribute('width', '256'); // Default prop value + }); + + it('supports .width and .height props', () => { + const testId = randomText(8); + const height = 345; + const width = 123; + render(); + const image = screen.getByTestId(testId); + expect(image).toBeDefined(); + expect(image).toHaveAttribute('height', String(height)); + expect(image).toHaveAttribute('width', String(width)); + }); + + it('supports .title property', () => { + const testId = randomText(8); + const title = randomText(16); + render(); + const image = screen.getByTestId(testId); + expect(image).toBeDefined(); + expect(image).toHaveAttribute('title', title); + expect(image).toHaveAttribute('alt', title); // When title is provided, it is used as alt + }); + + it('supports .alt property even when .title is provided', () => { + const testId = randomText(8); + const title = randomText(16); + const alt = randomText(32); + render(); + const image = screen.getByTestId(testId); + expect(image).toBeDefined(); + expect(image).toHaveAttribute('alt', alt); + expect(image).toHaveAttribute('title', title); + }); +}); diff --git a/client/src/components/common/AppImage/AppImage.tsx b/client/src/components/common/AppImage/AppImage.tsx new file mode 100644 index 0000000..9136e1d --- /dev/null +++ b/client/src/components/common/AppImage/AppImage.tsx @@ -0,0 +1,23 @@ +import { FunctionComponent } from 'react'; +import NextImage, { ImageProps } from 'next/image'; + +interface AppImageProps extends Omit { + alt?: string; // Make property optional as it was before NextJs v13 +} + +/** + * Application wrapper around NextJS image with some default props + * @component AppImage + */ +const AppImage: FunctionComponent = ({ + title, // Note: value has be destructed before usage as default value for other property + alt = title ?? 'Image', + height = 256, + width = 256, + ...restOfProps +}) => { + // Uses custom loader + unoptimized="true" to avoid NextImage warning https://nextjs.org/docs/api-reference/next/image#unoptimized + return ; +}; + +export default AppImage; diff --git a/client/src/components/common/AppImage/index.tsx b/client/src/components/common/AppImage/index.tsx new file mode 100644 index 0000000..04d5e0c --- /dev/null +++ b/client/src/components/common/AppImage/index.tsx @@ -0,0 +1,3 @@ +import AppImage from './AppImage'; + +export { AppImage as default, AppImage }; diff --git a/client/src/components/common/AppLink/AppLink.test.tsx b/client/src/components/common/AppLink/AppLink.test.tsx new file mode 100644 index 0000000..afa70f8 --- /dev/null +++ b/client/src/components/common/AppLink/AppLink.test.tsx @@ -0,0 +1,229 @@ +import { render, screen } from '@testing-library/react'; +import mockRouter from 'next-router-mock'; +/* IMPORTANT! To get 'next/router' working with tests, add into "jest.setup.js" file following: +--- +jest.mock('next/router', () => require('next-router-mock')); +--- +*/ +import AppLink from '.'; +import { capitalize, randomColor } from '@/utils'; + +jest.mock('next/navigation', () => { + const result = { + ...require('next-router-mock'), + // useSearchParams: () => jest.fn(), + usePathname: () => { + const router = mockRouter; + return router.asPath; + }, + }; + return result; +}); + +/** + * AppLink wrapped with Mocked Router + */ +const ComponentToTest = AppLink; + +/** + * Tests for component + */ +describe(' component', () => { + it('renders itself', () => { + const text = 'sample text'; + const url = 'https://example.com/'; + render({text}); + const link = screen.getByText(text); + expect(link).toBeDefined(); + expect(link).toHaveAttribute('href', url); + expect(link).toHaveTextContent(text); + }); + + it('supports external link', () => { + const text = 'external link'; + const url = 'https://example.com/'; + render({text}); + const link = screen.getByText(text); + expect(link).toBeDefined(); + expect(link).toHaveAttribute('href', url); + expect(link).toHaveTextContent(text); + expect(link).toHaveAttribute('target', '_blank'); // Open external links in new Tab by default + expect(link).toHaveAttribute('rel'); // For links opened in new Tab rel="noreferrer noopener" is required + const rel = (link as any)?.rel; + expect(rel.includes('noreferrer')).toBeTruthy(); // ref="noreferrer" check + expect(rel.includes('noopener')).toBeTruthy(); // rel="noreferrer check + }); + + it('supports internal link', () => { + const text = 'internal link'; + const url = '/internal-link'; + render({text}); + const link = screen.getByText(text); + expect(link).toBeDefined(); + expect(link).toHaveAttribute('href', url); + expect(link).toHaveTextContent(text); + expect(link).not.toHaveAttribute('target'); + expect(link).not.toHaveAttribute('rel'); + }); + + it('supports .openInNewTab property', () => { + // External link with openInNewTab={false} + let text = 'external link in same tab'; + let url = 'https://example.com/'; + render( + + {text} + + ); + let link = screen.getByText(text); + expect(link).toBeDefined(); + expect(link).toHaveAttribute('href', url); + expect(link).toHaveTextContent(text); + expect(link).not.toHaveAttribute('target'); + expect(link).not.toHaveAttribute('rel'); + + // Internal link with openInNewTab={true} + text = 'internal link in new tab'; + url = '/internal-link-in-new-tab'; + render( + + {text} + + ); + link = screen.getByText(text); + expect(link).toBeDefined(); + expect(link).toHaveAttribute('href', url); + expect(link).toHaveTextContent(text); + expect(link).toHaveAttribute('target', '_blank'); // Open links in new Tab + expect(link).toHaveAttribute('rel'); // For links opened in new Tab rel="noreferrer noopener" is required + const rel = (link as any)?.rel; + expect(rel.includes('noreferrer')).toBeTruthy(); // ref="noreferrer" check + expect(rel.includes('noopener')).toBeTruthy(); // rel="noreferrer check + }); + + it('supports .className property', () => { + let text = 'internal link with specific class'; + let url = '/internal-link-with-class'; + let className = 'someClassName'; + render( + + {text} + + ); + let link = screen.getByText(text); + expect(link).toBeDefined(); + expect(link).toHaveClass(className); + }); + + it('supports .activeClassName property in pair with .to property', () => { + let link; + let textActive = 'internal link with activeClassName'; + let textPassive = 'internal link without activeClassName'; + let url = '/internal-link'; + let activeClassName = 'someClassName'; + + // router.pathhname doesn't match .to prop + mockRouter.push('not-' + url); + render( + + {textPassive} + + ); + link = screen.getByText(textPassive); + expect(link).toBeDefined(); + expect(link).not.toHaveClass(activeClassName); + + // router.pathhname matches .to prop + mockRouter.push(url); + render( + + {textActive} + + ); + link = screen.getByText(textActive); + expect(link).toBeDefined(); + expect(link).toHaveClass(activeClassName); + }); + + it('supports .activeClassName property in pair with .href property', () => { + let link; + let textActive = 'external link with activeClassName'; + let textPassive = 'external link without activeClassName'; + let url = '/external-link.com'; + let activeClassName = 'someClassName'; + + // router.pathhname doesn't match .href prop + mockRouter.push('not-' + url); + render( + + {textPassive} + + ); + link = screen.getByText(textPassive); + expect(link).toBeDefined(); + expect(link).not.toHaveClass(activeClassName); + + // router.pathhname matches .href prop + mockRouter.push(url); + render( + + {textActive} + + ); + link = screen.getByText(textActive); + expect(link).toBeDefined(); + expect(link).toHaveClass(activeClassName); + }); + + it('supports .color property', () => { + // Check several times with random colors + for (let i = 1; i < 5; i++) { + let text = `link #${i} with .color property`; + let url = '/internal-link-with-color'; + let color = randomColor(); + render( + + {text} + + ); + let link = screen.getByText(text); + expect(link).toBeDefined(); + expect(link).toHaveStyle(`color: ${color}`); + } + }); + + it('supports .underline property', () => { + // Enumerate all possible values + ['hover', 'always', 'none'].forEach((underline) => { + let text = `link with .underline == "${underline}"`; + let url = '/internal-link-with-underline'; + render( + + {text} + + ); + let link = screen.getByText(text); + expect(link).toBeDefined(); + underline === 'none' + ? expect(link).toHaveStyle('text-decoration: none') + : expect(link).toHaveStyle('text-decoration: underline'); + // TODO: make "hover" test with "mouse moving" + + expect(link).toHaveClass(`MuiLink-underline${capitalize(underline)}`); + }); + }); + + it('supports .noLinkStyle property', () => { + let text = 'internal link noLinkStyle'; + let url = '/internal-link-no-style'; + let noLinkStyle = true; + render( + + {text} + + ); + let link = screen.getByText(text); + expect(link).toBeDefined(); + expect(link).not.toHaveClass('MuiLink-root'); + }); +}); diff --git a/client/src/components/common/AppLink/AppLinkNextNavigation.tsx b/client/src/components/common/AppLink/AppLinkNextNavigation.tsx new file mode 100644 index 0000000..4e4ac4a --- /dev/null +++ b/client/src/components/common/AppLink/AppLinkNextNavigation.tsx @@ -0,0 +1,136 @@ +'use client'; +// See: https://github.com/mui-org/material-ui/blob/6b18675c7e6204b77f4c469e113f62ee8be39178/examples/nextjs-with-typescript/src/Link.tsx +/* eslint-disable jsx-a11y/anchor-has-content */ +import { AnchorHTMLAttributes, forwardRef } from 'react'; +import clsx from 'clsx'; +import { usePathname } from 'next/navigation'; +import NextLink, { LinkProps as NextLinkProps } from 'next/link'; +import MuiLink, { LinkProps as MuiLinkProps } from '@mui/material/Link'; +import { APP_LINK_COLOR, APP_LINK_UNDERLINE } from '../../config'; + +export const EXTERNAL_LINK_PROPS = { + target: '_blank', + rel: 'noopener noreferrer', +}; + +/** + * Props for NextLinkComposed component + */ +interface NextLinkComposedProps + extends Omit, 'href'>, + Omit { + to: NextLinkProps['href']; + linkAs?: NextLinkProps['as']; + href?: NextLinkProps['href']; +} + +/** + * NextJS composed link to use with Material UI + * @NextLinkComposed NextLinkComposed + */ +const NextLinkComposed = forwardRef(function NextLinkComposed( + { to, linkAs, href, replace, scroll, passHref, shallow, prefetch, ...restOfProps }, + ref +) { + return ( + + + + ); +}); + +/** + * Props for AppLinkForNext component + */ +export type AppLinkForNextProps = { + activeClassName?: string; + as?: NextLinkProps['as']; + href?: string | NextLinkProps['href']; + noLinkStyle?: boolean; + to?: string | NextLinkProps['href']; + openInNewTab?: boolean; +} & Omit & + Omit; + +/** + * Material UI link for NextJS + * A styled version of the Next.js Link component: https://nextjs.org/docs/#with-link + * @component AppLinkForNext + * @param {string} [activeClassName] - class name for active link, applied when the router.pathname matches .href or .to props + * @param {string} [as] - passed to NextJS Link component in .as prop + * @param {string} [className] - class name for tag or NextJS Link component + * @param {object|function} children - content to wrap with tag + * @param {string} [color] - color of the link + * @param {boolean} [noLinkStyle] - when true, link will not have MUI styles + * @param {string} [to] - internal link URI + * @param {string} [href] - external link URI + * @param {boolean} [openInNewTab] - link will be opened in new tab when true + * @param {string} [underline] - controls "underline" style of the MUI link: 'hover' | 'always' | 'none' + */ +const AppLinkForNext = forwardRef(function Link(props, ref) { + const { + activeClassName = 'active', // This class is applied to the Link component when the router.pathname matches the href/to prop + as: linkAs, + className: classNameProps, + href, + noLinkStyle, + role, // Link don't have roles, so just exclude it from ...restOfProps + color = APP_LINK_COLOR, + underline = APP_LINK_UNDERLINE, + to, + sx, + openInNewTab = Boolean(href), // Open external links in new Tab by default + ...restOfProps + } = props; + const currentPath = usePathname(); + const destination = to ?? href ?? ''; + const pathname = typeof destination === 'string' ? destination : destination.pathname; + const className = clsx(classNameProps, { + [activeClassName]: pathname == currentPath && activeClassName, + }); + + const isExternal = + typeof destination === 'string' && (destination.startsWith('http') || destination.startsWith('mailto:')); + + const propsToRender = { + color, + underline, // 'hover' | 'always' | 'none' + ...(openInNewTab && EXTERNAL_LINK_PROPS), + ...restOfProps, + }; + + if (isExternal) { + if (noLinkStyle) { + return ; + } + + return ; + } + + if (noLinkStyle) { + return ; + } + + return ( + + ); +}); + +export default AppLinkForNext; diff --git a/client/src/components/common/AppLink/index.tsx b/client/src/components/common/AppLink/index.tsx new file mode 100644 index 0000000..5afd97d --- /dev/null +++ b/client/src/components/common/AppLink/index.tsx @@ -0,0 +1,4 @@ +import AppLink, { AppLinkForNextProps as AppLinkProps } from './AppLinkNextNavigation'; + +export type { AppLinkProps }; +export { AppLink as default, AppLink }; diff --git a/client/src/components/common/AppLoading/AppLoading.tsx b/client/src/components/common/AppLoading/AppLoading.tsx new file mode 100644 index 0000000..2ae7997 --- /dev/null +++ b/client/src/components/common/AppLoading/AppLoading.tsx @@ -0,0 +1,36 @@ +import { FunctionComponent } from 'react'; +import { CircularProgress, CircularProgressProps, LinearProgress, Stack, StackProps } from '@mui/material'; +import { APP_LOADING_COLOR, APP_LOADING_SIZE, APP_LOADING_TYPE } from '@/components/config'; + +interface Props extends StackProps { + color?: CircularProgressProps['color']; + size?: number | string; + type?: 'circular' | 'linear'; + value?: number; +} + +/** + * Renders MI circular progress centered inside Stack + * @component AppLoading + * @prop {string} [size] - size of the progress component. Numbers means pixels, string can be '2.5rem' + */ +const AppLoading: FunctionComponent = ({ + color = APP_LOADING_COLOR, + size = APP_LOADING_SIZE, + type = APP_LOADING_TYPE, + value, + ...restOfProps +}) => { + const alignItems = type === 'linear' ? undefined : 'center'; + return ( + + {type === 'linear' ? ( + + ) : ( + + )} + + ); +}; + +export default AppLoading; diff --git a/client/src/components/common/AppLoading/index.tsx b/client/src/components/common/AppLoading/index.tsx new file mode 100644 index 0000000..c60caaf --- /dev/null +++ b/client/src/components/common/AppLoading/index.tsx @@ -0,0 +1,4 @@ +import AppLoading from './AppLoading'; + +export { AppLoading }; +export default AppLoading; diff --git a/client/src/components/common/ErrorBoundary.tsx b/client/src/components/common/ErrorBoundary.tsx new file mode 100644 index 0000000..f5d907b --- /dev/null +++ b/client/src/components/common/ErrorBoundary.tsx @@ -0,0 +1,61 @@ +'use client'; +import { Component, ErrorInfo, ReactNode } from 'react'; + +interface Props { + children: ReactNode; + name: string; +} + +interface State { + hasError: boolean; + error?: Error; + errorInfo?: ErrorInfo; +} + +/** + * Error boundary wrapper to save Application parts from falling + * @component ErrorBoundary + * @param {string} [props.name] - name of the wrapped segment, "Error Boundary" by default + */ +class ErrorBoundary extends Component { + static defaultProps = { + name: 'Error Boundary', + }; + + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error) { + // The next render will show the Error UI + return { hasError: true }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // Save information to help render Error UI + this.setState({ error, errorInfo }); + // TODO: Add log error messages to an error reporting service here + } + + render() { + if (this.state.hasError) { + // Error UI rendering + return ( +
+

{this.props.name} - Something went wrong

+
+ {this.state?.error?.toString()} +
+ {this.state?.errorInfo?.componentStack} +
+
+ ); + } + + // Normal UI rendering + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/client/src/components/common/index.tsx b/client/src/components/common/index.tsx new file mode 100644 index 0000000..ace3ece --- /dev/null +++ b/client/src/components/common/index.tsx @@ -0,0 +1,10 @@ +import AppAlert from './AppAlert'; +import AppButton from './AppButton'; +import AppIcon from './AppIcon'; +import AppIconButton from './AppIconButton'; +import AppImage from './AppImage'; +import AppLink from './AppLink'; +import AppLoading from './AppLoading'; +import ErrorBoundary from './ErrorBoundary'; + +export { ErrorBoundary, AppAlert, AppButton, AppIcon, AppIconButton, AppImage, AppLink, AppLoading }; diff --git a/client/src/components/config.ts b/client/src/components/config.ts new file mode 100644 index 0000000..a4ae27d --- /dev/null +++ b/client/src/components/config.ts @@ -0,0 +1,35 @@ +/** + * Components configuration + */ +export const CONTENT_MAX_WIDTH = 800; +export const CONTENT_MIN_WIDTH = 320; // CONTENT_MAX_WIDTH - Sidebar width + +/** + * AppAlert and AppSnackBarAlert components + */ +export const APP_ALERT_SEVERITY = 'error'; // 'error' | 'info'| 'success' | 'warning' +export const APP_ALERT_VARIANT = 'filled'; // 'filled' | 'outlined' | 'standard' + +/** + * AppButton component + */ +export const APP_BUTTON_VARIANT = 'contained'; // | 'text' | 'outlined' +export const APP_BUTTON_MARGIN = 1; + +/** + * AppIcon component + */ +export const APP_ICON_SIZE = 24; + +/** + * AppLink component + */ +export const APP_LINK_COLOR = 'textSecondary'; // 'primary' // 'secondary' +export const APP_LINK_UNDERLINE = 'hover'; // 'always + +/** + * AppLoading component + */ +export const APP_LOADING_COLOR = 'primary'; // 'secondary' +export const APP_LOADING_SIZE = '3rem'; // 40 +export const APP_LOADING_TYPE = 'circular'; // 'linear'; // 'circular' diff --git a/client/src/components/index.tsx b/client/src/components/index.tsx new file mode 100644 index 0000000..1277447 --- /dev/null +++ b/client/src/components/index.tsx @@ -0,0 +1,5 @@ +export * from './common'; + +import UserInfo from './UserInfo'; + +export { UserInfo }; diff --git a/client/src/config.ts b/client/src/config.ts new file mode 100644 index 0000000..11a5479 --- /dev/null +++ b/client/src/config.ts @@ -0,0 +1,15 @@ +import { envRequired, getCurrentEnvironment } from '@/utils/environment'; + +export const IS_DEBUG = process.env.NEXT_PUBLIC_DEBUG === 'true'; // Enables logging, etc. + +export const IS_PRODUCTION = getCurrentEnvironment() === 'production'; // Enables analytics, etc. + +// export const PUBLIC_URL = envRequired(process.env.NEXT_PUBLIC_PUBLIC_URL); // Variant 1: .env variable is required +export const PUBLIC_URL = process.env.NEXT_PUBLIC_PUBLIC_URL; // Variant 2: .env variable is optional + +IS_DEBUG && + console.log('@/config', { + IS_DEBUG, + IS_PRODUCTION, + PUBLIC_URL, + }); diff --git a/client/src/hooks/auth.ts b/client/src/hooks/auth.ts new file mode 100644 index 0000000..27b8171 --- /dev/null +++ b/client/src/hooks/auth.ts @@ -0,0 +1,32 @@ +import { useCallback } from 'react'; +import { sessionStorageGet, sessionStorageDelete } from '@/utils/sessionStorage'; +import { useAppStore } from '../store'; + +/** + * Hook to detect is current user authenticated or not + * @returns {boolean} true if user is authenticated, false otherwise + */ +export function useIsAuthenticated() { + const [state] = useAppStore(); + let result = state.isAuthenticated; + + // TODO: AUTH: replace next line with access token verification + result = Boolean(sessionStorageGet('access_token', '')); + + return result; +} + +/** + * Returns event handler to Logout current user + * @returns {function} calling this event logs out current user + */ +export function useEventLogout() { + const [, dispatch] = useAppStore(); + + return useCallback(() => { + // TODO: AUTH: replace next line with access token saving + sessionStorageDelete('access_token'); + + dispatch({ type: 'LOG_OUT' }); + }, [dispatch]); +} diff --git a/client/src/hooks/event.ts b/client/src/hooks/event.ts new file mode 100644 index 0000000..adc2136 --- /dev/null +++ b/client/src/hooks/event.ts @@ -0,0 +1,17 @@ +import { useCallback } from 'react'; +import { useAppStore } from '../store'; + +/** + * Returns event handler to toggle Dark/Light modes + * @returns {function} calling this event toggles dark/light mode + */ +export function useEventSwitchDarkMode() { + const [state, dispatch] = useAppStore(); + + return useCallback(() => { + dispatch({ + type: 'DARK_MODE', + payload: !state.darkMode, + }); + }, [state, dispatch]); +} diff --git a/client/src/hooks/index.ts b/client/src/hooks/index.ts new file mode 100644 index 0000000..13ede56 --- /dev/null +++ b/client/src/hooks/index.ts @@ -0,0 +1,3 @@ +export * from './auth'; +export * from './event'; +export * from './layout'; diff --git a/client/src/hooks/layout.ts b/client/src/hooks/layout.ts new file mode 100644 index 0000000..34c4d58 --- /dev/null +++ b/client/src/hooks/layout.ts @@ -0,0 +1,76 @@ +'use client'; +import { useEffect, useState } from 'react'; +import useWindowsSize from './useWindowSize'; +import { useMediaQuery, useTheme } from '@mui/material'; +import { IS_SERVER } from '@/utils'; + +export const MOBILE_SCREEN_MAX_WIDTH = 600; // Sync with https://mui.com/material-ui/customization/breakpoints/ +export const SERVER_SIDE_MOBILE_FIRST = true; // true - for mobile, false - for desktop + +/** + * Hook to detect onMobile vs. onDesktop using "resize" event listener + * @returns {boolean} true when on onMobile, false when on onDesktop + */ +export function useIsMobileByWindowsResizing() { + const theme = useTheme(); + const { width } = useWindowsSize(); + const onMobile = width <= theme.breakpoints?.values?.sm ?? MOBILE_SCREEN_MAX_WIDTH; + return onMobile; +} + +/** + * Hook to detect onMobile vs. onDesktop using Media Query + * @returns {boolean} true when on onMobile, false when on onDesktop + */ +function useIsMobileByMediaQuery() { + // const onMobile = useMediaQuery({ maxWidth: MOBILE_SCREEN_MAX_WIDTH }); + const theme = useTheme(); + const onMobile = useMediaQuery(theme.breakpoints.down('sm')); + return onMobile; +} + +/** + * Hook to detect onMobile vs. onDesktop with Next.js workaround + * @returns {boolean} true when on onMobile, false when on onDesktop + */ +function useIsMobileForNextJs() { + // const onMobile = useOnMobileByWindowsResizing(); + const onMobile = useIsMobileByMediaQuery(); + const [onMobileDelayed, setOnMobileDelayed] = useState(SERVER_SIDE_MOBILE_FIRST); + + useEffect(() => { + setOnMobileDelayed(onMobile); // Next.js don't allow to use useOnMobileXxx() directly, so we need to use this workaround + }, [onMobile]); + + return onMobileDelayed; +} + +/** + * Hook to apply "onMobile" vs. "onDesktop" class to document.body depending on screen size. + * Due to SSR/SSG we can not set 'app-layout onMobile' or 'app-layout onDesktop' on the server + * If we modify className using JS, we will got Warning: Prop `className` did not match. Server: "app-layout" Client: "app-layout onDesktop" + * So we have to apply document.body.class using the hook :) + * Note: Use this hook one time only! In main App or Layout component + */ +function useMobileOrDesktopByChangingBodyClass() { + // const onMobile = useOnMobileByWindowsResizing(); + const onMobile = useIsMobileByMediaQuery(); + + useEffect(() => { + if (onMobile) { + document.body.classList.remove('onDesktop'); + document.body.classList.add('onMobile'); + } else { + document.body.classList.remove('onMobile'); + document.body.classList.add('onDesktop'); + } + }, [onMobile]); +} + +/** + * We need a "smart export wrappers", because we can not use hooks on the server side + */ +// export const useOnMobile = IS_SERVER ? () => SERVER_SIDE_IS_MOBILE_VALUE : useOnMobileByWindowsResizing; +// export const useOnMobile = IS_SERVER ? () => SERVER_SIDE_IS_MOBILE_VALUE : useOnMobileByMediaQuery; +export const useIsMobile = IS_SERVER ? () => SERVER_SIDE_MOBILE_FIRST : useIsMobileForNextJs; +export const useBodyClassForMobileOrDesktop = IS_SERVER ? () => undefined : useMobileOrDesktopByChangingBodyClass; diff --git a/client/src/hooks/useWindowSize.ts b/client/src/hooks/useWindowSize.ts new file mode 100644 index 0000000..16fb0b7 --- /dev/null +++ b/client/src/hooks/useWindowSize.ts @@ -0,0 +1,40 @@ +import { useLayoutEffect, useState } from 'react'; +import { IS_SERVER } from '@/utils'; + +const MOBILE_WINDOWS_SIZE = { width: 720, height: 1280 }; +const DESKTOP_WINDOWS_SIZE = { width: 1920, height: 1080 }; +const DEFAULT_WINDOWS_SIZE = MOBILE_WINDOWS_SIZE ?? DESKTOP_WINDOWS_SIZE; // Mobile-First by default + +type WindowSize = { + width: number; + height: number; +}; + +/** + * Hook to monitor Window (actually Browser) Size using "resize" event listener + * @returns {WindowSize} current window size as {width, height} object + */ +const useWindowSize = (): WindowSize => { + const [windowSize, setWindowSize] = useState(DEFAULT_WINDOWS_SIZE); + + useLayoutEffect(() => { + function handleResize() { + setWindowSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + } + + window.addEventListener('resize', handleResize); + handleResize(); // Get initial/current window size + + return () => window.removeEventListener('resize', handleResize); + }, []); + + return windowSize; +}; + +/** + * The hook will really work in Browser only, so or Server Side Rendering (SSR) we just return DEFAULT_WINDOWS_SIZE + */ +export default IS_SERVER ? () => DEFAULT_WINDOWS_SIZE : useWindowSize; diff --git a/client/src/layout/CurrentLayout.tsx b/client/src/layout/CurrentLayout.tsx new file mode 100644 index 0000000..8062b61 --- /dev/null +++ b/client/src/layout/CurrentLayout.tsx @@ -0,0 +1,15 @@ +'use client'; +import React, { FunctionComponent, PropsWithChildren } from 'react'; +import { useIsAuthenticated } from '@/hooks'; +import PrivateLayout from './PrivateLayout'; +import PublicLayout from './PublicLayout'; + +/** + * Returns the current Layout component depending on different circumstances. + * @layout CurrentLayout + */ +const CurrentLayout: FunctionComponent = (props) => { + return useIsAuthenticated() ? : ; +}; + +export default CurrentLayout; diff --git a/client/src/layout/PrivateLayout.tsx b/client/src/layout/PrivateLayout.tsx new file mode 100644 index 0000000..9550ad9 --- /dev/null +++ b/client/src/layout/PrivateLayout.tsx @@ -0,0 +1,49 @@ +import { FunctionComponent, PropsWithChildren } from 'react'; +import { LinkToPage } from '@/utils'; +import TopBarAndSideBarLayout from './TopBarAndSideBarLayout'; + +const TITLE_PRIVATE = 'Balinyaar'; // Title for pages after authentication + +/** + * SideBar navigation items with links for Private Layout + */ +const SIDE_BAR_ITEMS: Array = [ + { + title: 'Home', + path: '/', + icon: 'home', + }, + { + title: 'My Profile', + path: '/me', + icon: 'account', + }, + { + title: '404', + path: '/wrong-url', + icon: 'error', + }, + { + title: 'About', + path: '/about', + icon: 'info', + }, +]; + +/** + * Renders "Private Layout" composition + * @layout PrivateLayout + */ +const PrivateLayout: FunctionComponent = ({ children }) => { + const title = TITLE_PRIVATE; + document.title = title; // Also Update Tab Title // TODO: Do we need this? Move it to useEffect()? + + return ( + + {children} + {/* Copyright © */} + + ); +}; + +export default PrivateLayout; diff --git a/client/src/layout/PublicLayout.tsx b/client/src/layout/PublicLayout.tsx new file mode 100644 index 0000000..64a11c1 --- /dev/null +++ b/client/src/layout/PublicLayout.tsx @@ -0,0 +1,72 @@ +import { FunctionComponent, PropsWithChildren } from 'react'; +import { Stack } from '@mui/material'; +import { LinkToPage } from '@/utils'; +import { useIsMobile } from '@/hooks'; +import { BottomBar } from './components'; +import TopBarAndSideBarLayout from './TopBarAndSideBarLayout'; +import { BOTTOM_BAR_DESKTOP_VISIBLE } from './config'; + +const TITLE_PUBLIC = 'Unauthorized - Balinyaar'; // Title for pages without/before authentication + +/** + * SideBar navigation items with links for Public Layout + */ +const SIDE_BAR_ITEMS: Array = [ + { + title: 'Log In', + path: '/auth/login', + icon: 'login', + }, + { + title: 'Sign Up', + path: '/auth/signup', + icon: 'signup', + }, + { + title: 'About', + path: '/about', + icon: 'info', + }, +]; + +/** + * BottomBar navigation items with links for Public Layout + */ +const BOTTOM_BAR_ITEMS: Array = [ + { + title: 'Log In', + path: '/auth/login', + icon: 'login', + }, + { + title: 'Sign Up', + path: '/auth/signup', + icon: 'signup', + }, + { + title: 'About', + path: '/about', + icon: 'info', + }, +]; + +/** + * Renders "Public Layout" composition + * @layout PublicLayout + */ +const PublicLayout: FunctionComponent = ({ children }) => { + const onMobile = useIsMobile(); + const bottomBarVisible = onMobile || BOTTOM_BAR_DESKTOP_VISIBLE; + + const title = TITLE_PUBLIC; + document.title = title; // Also Update Tab Title // TODO: Do we need this? Move it to useEffect()? + + return ( + + {children} + {bottomBarVisible && } + + ); +}; + +export default PublicLayout; diff --git a/client/src/layout/TopBarAndSideBarLayout.tsx b/client/src/layout/TopBarAndSideBarLayout.tsx new file mode 100644 index 0000000..3b3de39 --- /dev/null +++ b/client/src/layout/TopBarAndSideBarLayout.tsx @@ -0,0 +1,129 @@ +'use client'; +import { FunctionComponent, useMemo, useState } from 'react'; +import { Stack, StackProps } from '@mui/material'; +import { IS_DEBUG } from '@/config'; +import { AppIconButton, ErrorBoundary } from '@/components'; +import { useAppStore } from '@/store'; +import { LinkToPage } from '@/utils'; +import { useEventSwitchDarkMode, useIsMobile } from '@/hooks'; +import { TopBar } from './components'; +import SideBar, { SideBarProps } from './components/SideBar'; +import { + SIDE_BAR_DESKTOP_ANCHOR, + SIDE_BAR_MOBILE_ANCHOR, + SIDE_BAR_WIDTH, + TOP_BAR_DESKTOP_HEIGHT, + TOP_BAR_MOBILE_HEIGHT, +} from './config'; + +interface Props extends StackProps { + sidebarItems: Array; + title: string; + variant: 'sidebarAlwaysTemporary' | 'sidebarPersistentOnDesktop' | 'sidebarAlwaysPersistent'; +} + +/** + * Renders "TopBar and SideBar" composition + * @layout TopBarAndSideBarLayout + */ +const TopBarAndSideBarLayout: FunctionComponent = ({ children, sidebarItems, title, variant }) => { + const [state] = useAppStore(); + const [sidebarVisible, setSidebarVisible] = useState(false); // TODO: Verify is default value is correct + const onMobile = useIsMobile(); + const onSwitchDarkMode = useEventSwitchDarkMode(); + + const sidebarProps = useMemo((): Partial => { + const anchor = onMobile ? SIDE_BAR_MOBILE_ANCHOR : SIDE_BAR_DESKTOP_ANCHOR; + let open = sidebarVisible; + let sidebarVariant: SideBarProps['variant'] = 'temporary'; + switch (variant) { + case 'sidebarAlwaysTemporary': + break; + case 'sidebarPersistentOnDesktop': + open = onMobile ? sidebarVisible : true; + sidebarVariant = onMobile ? 'temporary' : 'persistent'; + break; + case 'sidebarAlwaysPersistent': + open = true; + sidebarVariant = 'persistent'; + break; + } + return { anchor, open, variant: sidebarVariant }; + }, [onMobile, sidebarVisible, variant]); + + const stackStyles = useMemo( + () => ({ + minHeight: '100vh', // Full screen height + paddingTop: onMobile ? TOP_BAR_MOBILE_HEIGHT : TOP_BAR_DESKTOP_HEIGHT, + paddingLeft: + sidebarProps.variant === 'persistent' && sidebarProps.open && sidebarProps?.anchor?.includes('left') + ? SIDE_BAR_WIDTH + : undefined, + paddingRight: + sidebarProps.variant === 'persistent' && sidebarProps.open && sidebarProps?.anchor?.includes('right') + ? SIDE_BAR_WIDTH + : undefined, + }), + [onMobile, sidebarProps] + ); + + const onSideBarOpen = () => { + if (!sidebarVisible) setSidebarVisible(true); // Don't re-render Layout when SideBar is already open + }; + + const onSideBarClose = () => { + if (sidebarVisible) setSidebarVisible(false); // Don't re-render Layout when SideBar is already closed + }; + + const LogoButton = ( + + ); + + const DarkModeButton = ( + + ); + + // Note: useMemo() is not needed for startNode, endNode. We need respect store.darkMode and so on. + const { startNode, endNode } = sidebarProps?.anchor?.includes('left') + ? { startNode: LogoButton, endNode: DarkModeButton } + : { startNode: DarkModeButton, endNode: LogoButton }; + + IS_DEBUG && + console.log('Render ', { + onMobile, + darkMode: state.darkMode, + sidebarProps, + }); + + return ( + + + + + + + + {children} + + + ); +}; + +export default TopBarAndSideBarLayout; diff --git a/client/src/layout/components/BottomBar.tsx b/client/src/layout/components/BottomBar.tsx new file mode 100644 index 0000000..8169847 --- /dev/null +++ b/client/src/layout/components/BottomBar.tsx @@ -0,0 +1,39 @@ +'use client'; +import { FunctionComponent, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; +import { BottomNavigation, BottomNavigationAction } from '@mui/material'; +import { LinkToPage } from '@/utils'; +import { AppIcon } from '@/components'; + +interface Props { + items: Array; +} + +/** + * Renders horizontal Navigation Bar using MUI BottomNavigation component + * @component BottomBar + */ +const BottomBar: FunctionComponent = ({ items }) => { + const router = useRouter(); + + const onNavigationChange = useCallback( + (_event: unknown, newValue: string) => { + router.push(newValue); + }, + [router] + ); + + return ( + + {items.map(({ title, path, icon }) => ( + } /> + ))} + + ); +}; + +export default BottomBar; diff --git a/client/src/layout/components/SideBar.tsx b/client/src/layout/components/SideBar.tsx new file mode 100644 index 0000000..998945b --- /dev/null +++ b/client/src/layout/components/SideBar.tsx @@ -0,0 +1,97 @@ +import { FunctionComponent, useCallback, MouseEvent } from 'react'; +import { Stack, Divider, Drawer, DrawerProps, FormControlLabel, Switch, Tooltip } from '@mui/material'; +import { useAppStore } from '@/store'; +import { LinkToPage } from '@/utils'; +import { useEventLogout, useEventSwitchDarkMode, useIsAuthenticated, useIsMobile } from '@/hooks'; +import { AppIconButton, UserInfo } from '@/components'; +import { SIDE_BAR_WIDTH, TOP_BAR_DESKTOP_HEIGHT } from '../config'; +import SideBarNavList from './SideBarNavList'; + +export interface SideBarProps extends Pick { + items: Array; +} + +/** + * Renders SideBar with Menu and User details + * Actually for Authenticated users only, rendered in "Private Layout" + * @component SideBar + * @param {string} anchor - 'left' or 'right' + * @param {boolean} open - the Drawer is visible when true + * @param {string} variant - variant of the Drawer, one of 'permanent', 'persistent', 'temporary' + * @param {function} onClose - called when the Drawer is closing + */ +const SideBar: FunctionComponent = ({ anchor, open, variant, items, onClose, ...restOfProps }) => { + const [state] = useAppStore(); + // const isAuthenticated = state.isAuthenticated; // Variant 1 + const isAuthenticated = useIsAuthenticated(); // Variant 2 + const onMobile = useIsMobile(); + + const onSwitchDarkMode = useEventSwitchDarkMode(); + const onLogout = useEventLogout(); + + const handleAfterLinkClick = useCallback( + (event: MouseEvent) => { + if (variant === 'temporary' && typeof onClose === 'function') { + onClose(event, 'backdropClick'); + } + }, + [variant, onClose] + ); + + return ( + + + {isAuthenticated && ( + <> + + + + )} + + + + + + + + } + /> + + + {isAuthenticated && } + + + + ); +}; + +export default SideBar; diff --git a/client/src/layout/components/SideBarNavItem.tsx b/client/src/layout/components/SideBarNavItem.tsx new file mode 100644 index 0000000..620e79a --- /dev/null +++ b/client/src/layout/components/SideBarNavItem.tsx @@ -0,0 +1,45 @@ +'use client'; +import { FunctionComponent, MouseEventHandler } from 'react'; +import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; +import { AppIcon, AppLink } from '@/components'; +import { LinkToPage } from '@/utils'; +import { usePathname } from 'next/navigation'; + +interface Props extends LinkToPage { + openInNewTab?: boolean; + selected?: boolean; + onClick?: MouseEventHandler; +} + +/** + * Renders Navigation Item for SideBar, detects current url and sets selected state if needed + * @component SideBarNavItem + */ +const SideBarNavItem: FunctionComponent = ({ + openInNewTab, + icon, + path, + selected: propSelected = false, + subtitle, + title, + onClick, +}) => { + const pathname = usePathname(); + const selected = propSelected || (path && path.length > 1 && pathname.startsWith(path)) || false; + + return ( + + {icon && } + + + ); +}; + +export default SideBarNavItem; diff --git a/client/src/layout/components/SideBarNavList.tsx b/client/src/layout/components/SideBarNavList.tsx new file mode 100644 index 0000000..3eb24c9 --- /dev/null +++ b/client/src/layout/components/SideBarNavList.tsx @@ -0,0 +1,35 @@ +import { FunctionComponent, MouseEventHandler } from 'react'; +import List from '@mui/material/List'; +import { LinkToPage } from '@/utils'; +import SideBarNavItem from './SideBarNavItem'; + +interface Props { + items: Array; + showIcons?: boolean; + onClick?: MouseEventHandler; +} + +/** + * Renders list of Navigation Items inside SideBar + * @component SideBarNavList + * @param {array} items - list of objects to render as navigation items + * @param {boolean} [showIcons] - icons in navigation items are visible when true + * @param {function} [onAfterLinkClick] - optional callback called when some navigation item was clicked + */ +const SideBarNavList: FunctionComponent = ({ items, showIcons, onClick, ...restOfProps }) => { + return ( + + {items.map(({ icon, path, title }) => ( + + ))} + + ); +}; + +export default SideBarNavList; diff --git a/client/src/layout/components/TopBar.tsx b/client/src/layout/components/TopBar.tsx new file mode 100644 index 0000000..2371dc1 --- /dev/null +++ b/client/src/layout/components/TopBar.tsx @@ -0,0 +1,46 @@ +import { FunctionComponent, ReactNode } from 'react'; +import { AppBar, Toolbar, Typography } from '@mui/material'; + +interface Props { + endNode?: ReactNode; + startNode?: ReactNode; + title?: string; +} + +/** + * Renders TopBar composition + * @component TopBar + */ +const TopBar: FunctionComponent = ({ endNode, startNode, title = '', ...restOfProps }) => { + return ( + + + {startNode} + + + {title} + + + {endNode} + + + ); +}; + +export default TopBar; diff --git a/client/src/layout/components/index.tsx b/client/src/layout/components/index.tsx new file mode 100644 index 0000000..9cd972d --- /dev/null +++ b/client/src/layout/components/index.tsx @@ -0,0 +1,5 @@ +import BottomBar from './BottomBar'; +import SideBar from './SideBar'; +import TopBar from './TopBar'; + +export { BottomBar, SideBar, TopBar }; diff --git a/client/src/layout/config.ts b/client/src/layout/config.ts new file mode 100644 index 0000000..8ff57ec --- /dev/null +++ b/client/src/layout/config.ts @@ -0,0 +1,21 @@ +/** + * Layout configuration + */ + +/** + * SideBar configuration + */ +export const SIDE_BAR_MOBILE_ANCHOR = 'right'; // 'right'; +export const SIDE_BAR_DESKTOP_ANCHOR = 'left'; // 'right'; +export const SIDE_BAR_WIDTH = '240px'; + +/** + * TopBar configuration + */ +export const TOP_BAR_MOBILE_HEIGHT = '56px'; +export const TOP_BAR_DESKTOP_HEIGHT = '64px'; + +/** + * BottomBar configuration + */ +export const BOTTOM_BAR_DESKTOP_VISIBLE = false; // true; diff --git a/client/src/layout/index.tsx b/client/src/layout/index.tsx new file mode 100644 index 0000000..678f030 --- /dev/null +++ b/client/src/layout/index.tsx @@ -0,0 +1,6 @@ +import CurrentLayout from './CurrentLayout'; +import PrivateLayout from './PrivateLayout'; +import PublicLayout from './PublicLayout'; + +export { PublicLayout, PrivateLayout }; +export default CurrentLayout; diff --git a/client/src/store/AppReducer.ts b/client/src/store/AppReducer.ts new file mode 100644 index 0000000..9e4f1fb --- /dev/null +++ b/client/src/store/AppReducer.ts @@ -0,0 +1,46 @@ +import { Reducer } from 'react'; +import { localStorageSet } from '../utils/localStorage'; +import { AppStoreState } from './config'; + +/** + * Reducer for global AppStore using "Redux styled" actions + * @function AppReducer + * @param {object} state - current/default state + * @param {string} action.type - unique name of the action + * @param {string} action.action - alternate to action.type property, unique name of the action + * @param {*} [action.payload] - optional data object or the function to get data object + */ +const AppReducer: Reducer = (state, action) => { + // console.log('AppReducer() - action:', action); + switch (action.type || action.action) { + case 'CURRENT_USER': + return { + ...state, + currentUser: action?.currentUser || action?.payload, + }; + case 'SIGN_UP': + case 'LOG_IN': + return { + ...state, + isAuthenticated: true, + }; + case 'LOG_OUT': + return { + ...state, + isAuthenticated: false, + currentUser: undefined, // Also reset previous user data + }; + case 'DARK_MODE': { + const darkMode = action?.darkMode ?? action?.payload; + localStorageSet('darkMode', darkMode); + return { + ...state, + darkMode, + }; + } + default: + return state; + } +}; + +export default AppReducer; diff --git a/client/src/store/AppStore.tsx b/client/src/store/AppStore.tsx new file mode 100644 index 0000000..d7948ea --- /dev/null +++ b/client/src/store/AppStore.tsx @@ -0,0 +1,81 @@ +'use client'; +import { + createContext, + useReducer, + useContext, + FunctionComponent, + PropsWithChildren, + Dispatch, + ComponentType, +} from 'react'; +// import useMediaQuery from '@mui/material/useMediaQuery'; +import AppReducer from './AppReducer'; +import { localStorageGet } from '../utils/localStorage'; +import { IS_SERVER } from '../utils/environment'; +import { APP_STORE_INITIAL_STATE, AppStoreState } from './config'; + +/** + * Instance of React Context for global AppStore + */ +export type AppContextReturningType = [AppStoreState, Dispatch]; +const AppContext = createContext([APP_STORE_INITIAL_STATE, () => null]); + +/** + * Main global Store as HOC with React Context API + * @component AppStoreProvider + * import {AppStoreProvider} from './store' + * ... + * + * + * + */ +const AppStoreProvider: FunctionComponent = ({ children }) => { + // const prefersDarkMode = IS_SERVER ? false : useMediaQuery('(prefers-color-scheme: dark)'); // Note: Conditional hook is bad idea :( + const prefersDarkMode = IS_SERVER ? false : window.matchMedia('(prefers-color-scheme: dark)').matches; + const previousDarkMode = IS_SERVER ? false : Boolean(localStorageGet('darkMode', false)); + // const tokenExists = Boolean(loadToken()); + + const initialState: AppStoreState = { + ...APP_STORE_INITIAL_STATE, + darkMode: previousDarkMode || prefersDarkMode, + // isAuthenticated: tokenExists, + }; + const value: AppContextReturningType = useReducer(AppReducer, initialState); + + return {children}; +}; + +/** + * Hook to use the AppStore in functional components + * @hook useAppStore + * import {useAppStore} from './store' + * ... + * const [state, dispatch] = useAppStore(); + * OR + * const [state] = useAppStore(); + */ +const useAppStore = (): AppContextReturningType => useContext(AppContext); + +/** + * HOC to inject the ApStore to class component, also works for functional components + * @hok withAppStore + * import {withAppStore} from './store' + * ... + * class MyComponent + * + * render () { + * const [state, dispatch] = this.props.appStore; + * ... + * } + * ... + * export default withAppStore(MyComponent) + */ +interface WithAppStoreProps { + appStore: AppContextReturningType; +} +const withAppStore = (Component: ComponentType): FunctionComponent => + function ComponentWithAppStore(props) { + return ; + }; + +export { AppStoreProvider, useAppStore, withAppStore }; diff --git a/client/src/store/config.ts b/client/src/store/config.ts new file mode 100644 index 0000000..5089909 --- /dev/null +++ b/client/src/store/config.ts @@ -0,0 +1,16 @@ +/** + * Data structure of the AppStore state + */ +export interface AppStoreState { + darkMode: boolean; + isAuthenticated: boolean; + currentUser?: object | undefined; +} + +/** + * Initial values for the AppStore state + */ +export const APP_STORE_INITIAL_STATE: AppStoreState = { + darkMode: false, // Overridden by useMediaQuery('(prefers-color-scheme: dark)') in AppStore + isAuthenticated: false, // Overridden in AppStore by checking auth token +}; diff --git a/client/src/store/index.tsx b/client/src/store/index.tsx new file mode 100644 index 0000000..d420cc2 --- /dev/null +++ b/client/src/store/index.tsx @@ -0,0 +1,3 @@ +import { AppStoreProvider, useAppStore, withAppStore } from './AppStore'; + +export { AppStoreProvider, useAppStore, withAppStore }; diff --git a/client/src/theme/MuiThemeProviderForNextJs.tsx b/client/src/theme/MuiThemeProviderForNextJs.tsx new file mode 100644 index 0000000..ee50006 --- /dev/null +++ b/client/src/theme/MuiThemeProviderForNextJs.tsx @@ -0,0 +1,12 @@ +import { AppRouterCacheProvider } from '@mui/material-nextjs/v13-appRouter'; +import { FunctionComponent, PropsWithChildren } from 'react'; + +/** + * Platform-specific ThemeProvider for Next.js + * @component MuiThemeProviderForNextJs + */ +const MuiThemeProviderForNextJs: FunctionComponent = ({ children }) => { + return {children}; +}; + +export default MuiThemeProviderForNextJs; diff --git a/client/src/theme/ThemeProvider.tsx b/client/src/theme/ThemeProvider.tsx new file mode 100644 index 0000000..f027895 --- /dev/null +++ b/client/src/theme/ThemeProvider.tsx @@ -0,0 +1,43 @@ +'use client'; +import { FunctionComponent, PropsWithChildren, useEffect, useMemo, useState } from 'react'; +import { ThemeProvider as MuiThemeProvider, createTheme } from '@mui/material/styles'; + +import { useAppStore } from '../store'; +import DARK_THEME from './dark'; +import LIGHT_THEME from './light'; +import MuiThemeProviderForNextJs from './MuiThemeProviderForNextJs'; +import CssBaseline from '@mui/material/CssBaseline'; + +function getThemeByDarkMode(darkMode: boolean) { + return darkMode ? createTheme(DARK_THEME) : createTheme(LIGHT_THEME); +} + +/** + * Renders composition of Emotion's CacheProvider + MUI's ThemeProvider to wrap content of entire App + * The Light or Dark themes applied depending on global .darkMode state + * @component AppThemeProvider + */ +const AppThemeProvider: FunctionComponent = ({ children }) => { + const [state] = useAppStore(); + const [loading, setLoading] = useState(true); + + const currentTheme = useMemo( + () => getThemeByDarkMode(state.darkMode), + [state.darkMode] // Observe AppStore and re-create the theme when .darkMode changes + ); + + useEffect(() => setLoading(false), []); // Set .loading to false when the component is mounted + + if (loading) return null; // Don't render anything until the component is mounted + + return ( + + + + {children} + + + ); +}; + +export default AppThemeProvider; diff --git a/client/src/theme/colors.ts b/client/src/theme/colors.ts new file mode 100644 index 0000000..16add99 --- /dev/null +++ b/client/src/theme/colors.ts @@ -0,0 +1,27 @@ +import { PaletteOptions, SimplePaletteColorOptions } from '@mui/material'; + +const COLOR_PRIMARY: SimplePaletteColorOptions = { + main: '#64B5F6', + contrastText: '#000000', + // light: '#64B5F6', + // dark: '#64B5F6', +}; + +const COLOR_SECONDARY: SimplePaletteColorOptions = { + main: '#EF9A9A', + contrastText: '#000000', + // light: '#EF9A9A', + // dark: '#EF9A9A', +}; + +/** + * MUI colors set to use in theme.palette + */ +export const PALETTE_COLORS: Partial = { + primary: COLOR_PRIMARY, + secondary: COLOR_SECONDARY, + // error: COLOR_ERROR, + // warning: COLOR_WARNING; + // info: COLOR_INFO; + // success: COLOR_SUCCESS; +}; diff --git a/client/src/theme/dark.ts b/client/src/theme/dark.ts new file mode 100644 index 0000000..c00dbf8 --- /dev/null +++ b/client/src/theme/dark.ts @@ -0,0 +1,18 @@ +import { ThemeOptions } from '@mui/material'; +import { PALETTE_COLORS } from './colors'; + +/** + * MUI theme options for "Dark Mode" + */ +export const DARK_THEME: ThemeOptions = { + palette: { + mode: 'dark', + // background: { + // paper: '#424242', // Gray 800 - Background of "Paper" based component + // default: '#121212', + // }, + ...PALETTE_COLORS, + }, +}; + +export default DARK_THEME; diff --git a/client/src/theme/index.ts b/client/src/theme/index.ts new file mode 100644 index 0000000..90bffc9 --- /dev/null +++ b/client/src/theme/index.ts @@ -0,0 +1,10 @@ +import AppThemeProvider from './ThemeProvider'; +import DARK_THEME from './dark'; +import LIGHT_THEME from './light'; + +export { + LIGHT_THEME as default, // Change to DARK_THEME if you want to use dark theme as default + DARK_THEME, + LIGHT_THEME, + AppThemeProvider as ThemeProvider, +}; diff --git a/client/src/theme/light.ts b/client/src/theme/light.ts new file mode 100644 index 0000000..fa8a5be --- /dev/null +++ b/client/src/theme/light.ts @@ -0,0 +1,18 @@ +import { ThemeOptions } from '@mui/material'; +import { PALETTE_COLORS } from './colors'; + +/** + * MUI theme options for "Light Mode" + */ +export const LIGHT_THEME: ThemeOptions = { + palette: { + mode: 'light', + // background: { + // paper: '#f5f5f5', // Gray 100 - Background of "Paper" based component + // default: '#FFFFFF', + // }, + ...PALETTE_COLORS, + }, +}; + +export default LIGHT_THEME; diff --git a/client/src/utils/environment.ts b/client/src/utils/environment.ts new file mode 100644 index 0000000..eaf871a --- /dev/null +++ b/client/src/utils/environment.ts @@ -0,0 +1,53 @@ +export const IS_SERVER = typeof window === 'undefined'; +export const IS_BROWSER = typeof window !== 'undefined' && typeof window?.document !== 'undefined'; +/* eslint-disable no-restricted-globals */ +export const IS_WEBWORKER = + typeof self === 'object' && self.constructor && self.constructor.name === 'DedicatedWorkerGlobalScope'; +/* eslint-enable no-restricted-globals */ + +/** + * Returns the value of the environment variable with the given name, raises an error if it is required and not set. + * Note: My not work with Next.js on client-side code. + * @param {string} name - The name of the environment variable to get: e.g. XXX_YYY_PUBLIC_URL + * @param {boolean} [isRequired] - Whether the environment variable is required or not. + * @param {string} [defaultValue] - The default value to return if the environment variable is not set. + * @returns {string} The value of the environment variable with the given name. + */ +export function envGet( + name: string, + isRequired = false, + defaultValue: string | undefined = undefined +): string | undefined { + let variable = process.env[name]; // Classic way + // let variable = import.meta.env[name]; // Vite way + + if (typeof variable === 'undefined') { + if (isRequired) { + throw new Error(`Missing process.env.${name} variable`); + } + variable = defaultValue; + } + return variable; +} + +/** + * Verifies existence of environment variables, raises an error if it is required and not set. + * @example const MY_VARIABLE = requireEnv(process.env.MY_VARIABLE); + * @param {string} [passProcessDotEnvDotValueNameHere] - Pass a value of process.env.MY_VARIABLE here, not just a name! + * @returns {string} The value of incoming parameter. + * @throws Error "Missing .env variable!" + */ +export function envRequired(passProcessDotEnvDotValueNameHere: string | undefined): string { + if (typeof passProcessDotEnvDotValueNameHere === 'undefined') { + throw new Error('Missing .env variable!'); + } + return passProcessDotEnvDotValueNameHere; +} + +export function getCurrentVersion(): string { + return process.env.npm_package_version ?? process.env.NEXT_PUBLIC_VERSION ?? 'unknown'; +} + +export function getCurrentEnvironment(): string { + return process.env.NEXT_PUBLIC_ENV ?? process.env?.NODE_ENV ?? 'development'; +} diff --git a/client/src/utils/index.ts b/client/src/utils/index.ts new file mode 100644 index 0000000..5995ab0 --- /dev/null +++ b/client/src/utils/index.ts @@ -0,0 +1,7 @@ +export * from './environment'; +export * from './localStorage'; +export * from './navigation'; +export * from './sessionStorage'; +export * from './sleep'; +export * from './type'; +export * from './text'; diff --git a/client/src/utils/localStorage.ts b/client/src/utils/localStorage.ts new file mode 100644 index 0000000..175726d --- /dev/null +++ b/client/src/utils/localStorage.ts @@ -0,0 +1,56 @@ +import { IS_SERVER } from './environment'; + +/** + * Smartly reads value from localStorage + */ +export function localStorageGet(name: string, defaultValue: any = ''): string { + if (IS_SERVER) { + return defaultValue; // We don't have access to localStorage on the server + } + + const valueFromStore = localStorage.getItem(name); + if (valueFromStore === null) return defaultValue; // No value in store, return default one + + try { + const jsonParsed = JSON.parse(valueFromStore); + if (['boolean', 'number', 'bigint', 'string', 'object'].includes(typeof jsonParsed)) { + return jsonParsed; // We successfully parse JS value from the store + } + } catch (error) {} + + return valueFromStore; // Return string value as it is +} + +/** + * Smartly writes value into localStorage + */ +export function localStorageSet(name: string, value: any) { + if (IS_SERVER) { + return; // Do nothing on server side + } + if (typeof value === 'undefined') { + return; // Do not store undefined values + } + let valueAsString: string; + if (typeof value === 'object') { + valueAsString = JSON.stringify(value); + } else { + valueAsString = String(value); + } + + localStorage.setItem(name, valueAsString); +} + +/** + * Deletes value by name from localStorage, if specified name is empty entire localStorage is cleared. + */ +export function localStorageDelete(name: string) { + if (IS_SERVER) { + return; // Do nothing on server side + } + if (name) { + localStorage.removeItem(name); + } else { + localStorage.clear(); + } +} diff --git a/client/src/utils/navigation.ts b/client/src/utils/navigation.ts new file mode 100644 index 0000000..d61bda9 --- /dev/null +++ b/client/src/utils/navigation.ts @@ -0,0 +1,55 @@ +import { IS_BROWSER } from './environment'; + +export const EXTERNAL_LINK_PROPS = { + target: '_blank', + rel: 'noopener noreferrer', +}; + +/** + * Disables "Back" button for current page + * Usage: Call function in useEffect( ,[]) or directly + */ +export function disableBackNavigation() { + window.history.pushState(null, '', window.location.href); + window.onpopstate = function () { + window.history.go(1); + }; +} + +/** + * Navigates to the specified URL with options + */ +export function navigateTo(url: string, replaceInsteadOfPush = false, optionalTitle = '') { + if (replaceInsteadOfPush) { + window.history.replaceState(null, optionalTitle, url); + } else { + window.history.pushState(null, optionalTitle, url); + } +} + +/** + * For smooth scrolling to the specified element with optional offset + * @param {object} destinationElement - DOM element to scroll to + * @param {number} [verticalOffset] - optional vertical offset + * @param {object} [scrollingElement] - optional scrolling element, defaults to .window + * @param {string} [behavior] - optional scroll behavior, defaults to 'smooth' + */ +export function scrollIntoViewAdjusted( + destinationElement: Element | HTMLElement | null, + verticalOffset = 0, + scrollingElement?: Element | HTMLElement | null, + behavior: 'auto' | 'instant' | 'smooth' | undefined = 'smooth' +) { + if (!IS_BROWSER || !destinationElement) { + return; + } + + const rect = destinationElement.getBoundingClientRect(); + if (!rect || typeof rect.top === 'undefined') { + return; + } + + const top = rect.top - verticalOffset; + const elementToScroll = scrollingElement ?? window; + elementToScroll.scrollBy({ top, behavior }); +} diff --git a/client/src/utils/sessionStorage.ts b/client/src/utils/sessionStorage.ts new file mode 100644 index 0000000..0a91d92 --- /dev/null +++ b/client/src/utils/sessionStorage.ts @@ -0,0 +1,56 @@ +import { IS_SERVER } from './environment'; + +/** + * Smartly reads value from sessionStorage + */ +export function sessionStorageGet(name: string, defaultValue: any = ''): string { + if (IS_SERVER) { + return defaultValue; // We don't have access to sessionStorage on the server + } + + const valueFromStore = sessionStorage.getItem(name); + if (valueFromStore === null) return defaultValue; // No value in store, return default one + + try { + const jsonParsed = JSON.parse(valueFromStore); + if (['boolean', 'number', 'bigint', 'string', 'object'].includes(typeof jsonParsed)) { + return jsonParsed; // We successfully parse JS value from the store + } + } catch (error) {} + + return valueFromStore; // Return string value as it is +} + +/** + * Smartly writes value into sessionStorage + */ +export function sessionStorageSet(name: string, value: any) { + if (IS_SERVER) { + return; // Do nothing on server side + } + if (typeof value === 'undefined') { + return; // Do not store undefined values + } + let valueAsString: string; + if (typeof value === 'object') { + valueAsString = JSON.stringify(value); + } else { + valueAsString = String(value); + } + + sessionStorage.setItem(name, valueAsString); +} + +/** + * Deletes value by name from sessionStorage, if specified name is empty entire sessionStorage is cleared. + */ +export function sessionStorageDelete(name: string) { + if (IS_SERVER) { + return; // Do nothing on server side + } + if (name) { + sessionStorage.removeItem(name); + } else { + sessionStorage.clear(); + } +} diff --git a/client/src/utils/sleep.ts b/client/src/utils/sleep.ts new file mode 100644 index 0000000..9693961 --- /dev/null +++ b/client/src/utils/sleep.ts @@ -0,0 +1,9 @@ +/** + * Delays code executions for specific amount of time. Must be called with await! + * @param {number} interval - number of milliseconds to wait for + */ +export async function sleep(interval = 1000) { + return new Promise((resolve) => setTimeout(resolve, interval)); +} + +export default sleep; diff --git a/client/src/utils/text.ts b/client/src/utils/text.ts new file mode 100644 index 0000000..5a8a1b1 --- /dev/null +++ b/client/src/utils/text.ts @@ -0,0 +1,49 @@ +export const CHARS_NUMERIC = '0123456789'; +export const CHARS_ALPHA_LOWER = 'abcdefghijklmnopqrstuvwxyz'; +export const CHARS_ALPHA_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +export const CHARS_ALPHA_NUMERIC = CHARS_NUMERIC + CHARS_ALPHA_LOWER + CHARS_ALPHA_UPPER; + +/** + * Generate a random string of a given length using a given set of characters + * @param {number} length - the length of the string to generate + * @param {string} [allowedChars] - the set of characters to use in the string, defaults to all alphanumeric characters in upper and lower case + numbers + * @returns {string} - the generated string + */ +export function randomText(length: number, allowedChars = CHARS_ALPHA_NUMERIC) { + let result = ''; + const charLength = allowedChars.length; + let counter = 0; + while (counter < length) { + result += allowedChars.charAt(Math.floor(Math.random() * charLength)); + counter += 1; + } + return result; +} +/** + * Compare two strings including null and undefined values + * @param {string} a - the first string to compare + * @param {string} b - the second string to compare + * @returns {boolean} - true if the strings are the same or both null or undefined, false otherwise + */ +export function compareTexts(a: string | null | undefined, b: string | null | undefined) { + if (a === undefined || a === null || a === '') { + return b === undefined || b === null || b === ''; + } + return a === b; +} + +/** + * Capitalize the first letter of a string + * @param {string} s - the string to capitalize + * @returns {string} - the capitalized string + */ +export const capitalize = (s: string): string => s.charAt(0).toUpperCase() + s.substring(1); + +/** + * Generate a random color as #RRGGBB value + * @returns {string} - the generated color + */ +export function randomColor() { + const color = Math.floor(Math.random() * 16777215).toString(16); + return '#' + color; +} diff --git a/client/src/utils/type.ts b/client/src/utils/type.ts new file mode 100644 index 0000000..b439b6a --- /dev/null +++ b/client/src/utils/type.ts @@ -0,0 +1,12 @@ +// Helper to read object's properties as obj['name'] +export type ObjectPropByName = Record; + +/** + * Data for "Page Link" in SideBar adn other UI elements + */ +export type LinkToPage = { + icon?: string; // Icon name to use as + path?: string; // URL to navigate to + title?: string; // Title or primary text to display + subtitle?: string; // Sub-title or secondary text to display +}; diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 0000000..20ff83d --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + }, + "forceConsistentCasingInFileNames": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/server/.gitattributes b/server/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/server/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..0e12b5f --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,342 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# 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 +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# 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 +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb +/nuget.exe +/src/API/CleanArc.Web.Api/logs/log.json diff --git a/server/AGENTS.md b/server/AGENTS.md new file mode 100644 index 0000000..332d5f4 --- /dev/null +++ b/server/AGENTS.md @@ -0,0 +1,109 @@ +# AGENTS.md — Balinyaar Server + +Agent-oriented guide to the backend. For human setup/run instructions see [README.md](README.md). + +## Stack + +- **ASP.NET Core / .NET 10** (`net10.0`), Web API +- **Clean Architecture** (Domain → Application → Infrastructure → API) +- **CQRS** with **MediatR** (source-generated `Mediator`) +- **EF Core 10** + **SQL Server** (Repository + Unit of Work) +- **ASP.NET Core Identity** with **JWE** (signed + AES-encrypted JWT), **OTP**, and **dynamic permission** authorization +- **Mapster** (mapping), **FluentValidation** (validation), **Serilog** (logging), **OpenTelemetry** + **prometheus-net** (observability), **NSwag/Swagger** (OpenAPI), **Asp.Versioning** (API versioning) +- **xUnit** + **NSubstitute** (tests) +- Centralized NuGet versions in `Directory.Packages.props` + +## Commands (run from `server/`) + +| Task | Command | +| ---------- | --------------------------------------------------------------------------------- | +| Restore | `dotnet restore CleanArcTemplate.sln` | +| Build | `dotnet build CleanArcTemplate.sln` | +| Run API | `dotnet run --project src/API/CleanArc.Web.Api/CleanArc.Web.Api.csproj` | +| Test | `dotnet test CleanArcTemplate.sln` | +| Add migration | `dotnet ef migrations add --project src/Infrastructure/CleanArc.Infrastructure.Persistence --startup-project src/API/CleanArc.Web.Api` | + +**Startup project:** `src/API/CleanArc.Web.Api`. Default URL `https://localhost:5002`, Swagger at `/swagger`. On boot the app applies EF migrations and seeds default users (`Program.cs` → `ApplyMigrationsAsync()` / `SeedDefaultUsersAsync()`), so a reachable DB is required. + +## Projects by layer + +``` +src/ +├── Core/ +│ ├── CleanArc.Domain Entities (User, Order, Role...), BaseEntity, IEntity, ITimeModification +│ └── CleanArc.Application CQRS Features/, Contracts/ (interfaces), Models/, MediatR pipeline (Common/) +├── Infrastructure/ +│ ├── CleanArc.Infrastructure.Persistence ApplicationDbContext, Configuration/, Repositories/, Migrations/ +│ ├── CleanArc.Infrastructure.Identity Jwt/, Identity/ (Managers, Stores, PermissionManager, Seed), ServiceConfiguration/ +│ ├── CleanArc.Infrastructure.CrossCutting Logging (Serilog) +│ └── CleanArc.Infrastructure.Monitoring HealthCheck / OpenTelemetry / Prometheus configs +├── API/ +│ ├── CleanArc.Web.Api Program.cs, Controllers/V1/, appsettings*.json +│ ├── CleanArc.WebFramework BaseController, Filters/, Middlewares/, Swagger/, Attributes/ +│ └── Plugins/CleanArc.Web.Plugins.Grpc GrpcPluginStartup, Services/, ProtoModels/ +├── Shared/CleanArc.SharedKernel Extensions + validation base used by all layers +└── Tests/ CleanArc.Tests.Setup + CleanArc.Test.Infrastructure.Identity +``` + +Dependency direction points **inward**: Domain depends on nothing; Application depends on Domain; Infrastructure and API implement/consume Application's contracts. Never make Domain or Application reference Infrastructure or the API. + +## Startup wiring — `src/API/CleanArc.Web.Api/Program.cs` + +Service registration is composed from per-layer extension methods (in each project's `ServiceConfiguration`): + +``` +ConfigureHealthChecks() · SetupOpenTelemetry() +AddApplicationServices() // MediatR + validators + pipeline behaviors +RegisterIdentityServices(...) // Identity, JWT/JWE, authorization policies +AddPersistenceServices(...) // DbContext, UnitOfWork, repositories +AddWebFrameworkServices() // API versioning +AddSwagger("v1","v1.1") · RegisterValidatorsAsServices() · AddMapster() +ConfigureGrpcPluginServices() // gRPC plugin +``` + +Pipeline order: exception handling → Swagger → routing → **authentication → authorization** → controllers → metrics → health checks → `ConfigureGrpcPipeline()`. + +When adding infrastructure, expose it as an extension method and call it here rather than inlining into `Program.cs`. + +## CQRS — how a feature is shaped + +Features live under `CleanArc.Application/Features//{Commands|Queries}//`. A query example (`Features/Order/Queries/GetAllOrders/`): + +- `GetAllOrdersQuery.cs` — `record ... : IRequest>` +- `GetAllOrdersQueryHandler.cs` — `internal` handler; depends on `IUnitOfWork`, `IMapper`; returns `OperationResult` +- `GetAllOrdersQueryResult.cs` — the DTO returned + +Commands additionally implement `IValidatableModel` and declare FluentValidation rules; the `ValidateCommandBehavior` MediatR pipeline (`Application/Common/`) runs validators before the handler and surfaces errors in `OperationResult`. + +**To add a feature:** create the folder with the request + handler (+ result/validator), then call it from a controller via `_sender.Send(...)`. Contracts the handler needs go in `Application/Contracts/` and are implemented in Infrastructure. + +## Controllers & results + +- Controllers live in `CleanArc.Web.Api/Controllers/V1/` and inherit `BaseController` (`CleanArc.WebFramework/BaseController/BaseController.cs`), which exposes `UserId`/`UserName`/etc. from claims and maps `OperationResult` → `IActionResult`. +- All responses are wrapped in `OperationResult` (`Application/Models/Common/`): `Result`, `IsSuccess`, `ErrorMessages`, `IsNotFound`, `IsException`. Use the factory methods (`SuccessResult`, `FailureResult`, `NotFoundResult`). +- Protected endpoints use `[Authorize(ConstantPolicies.DynamicPermission)]`. + +## Persistence + +- `ApplicationDbContext` (`CleanArc.Infrastructure.Persistence/ApplicationDbContext.cs`) extends `IdentityDbContext<...>`; it auto-registers `IEntity` types and applies all `IEntityTypeConfiguration` from the assembly. +- Per-entity config in `Configuration/Config/`. Repositories in `Repositories/` derive from `BaseAsyncRepository`; expose them through `IUnitOfWork` (interface in `Application/Contracts/Persistence/`). Commit via `unitOfWork.CommitAsync()`. +- Migrations in `Migrations/`. Add new ones with the `dotnet ef` command above. + +## Identity & auth + +- Token service: `CleanArc.Infrastructure.Identity/Jwt/JwtService.cs` (`IJwtService`) — issues JWE (HMAC-SHA256 signed, AES-128 encrypted), refresh tokens, and OTP/phone-based tokens. +- Custom Identity managers/stores under `Identity/Manager/` and `Identity/Store/`. +- Dynamic permissions: `Identity/PermissionManager/` (`DynamicPermissionService`, `DynamicPermissionHandler`, `ConstantPolicies`). +- Settings from `appsettings.json` → `IdentitySettings` (`SecretKey`, `Encryptkey` = 16 chars, `Issuer`, `Audience`, lifetimes). + +## gRPC plugin + +`Plugins/CleanArc.Web.Plugins.Grpc` is a self-contained module mounted via Application Parts. `GrpcPluginStartup.cs` provides `ConfigureGrpcPluginServices()` / `ConfigureGrpcPipeline()` (called from `Program.cs`). Proto contracts in `ProtoModels/*.proto`, services in `Services/`. The host uses HTTP/2 (`Kestrel` config) for gRPC. + +## Conventions + +- Add cross-layer wiring as `ServiceConfiguration` extension methods, not inline in `Program.cs`. +- Keep handlers `internal`; return `OperationResult`; don't throw for expected failures (use `FailureResult`/`NotFoundResult`). +- Use Mapster for entity↔DTO mapping; FluentValidation for input validation. +- Centralize package versions in `Directory.Packages.props` (no inline `Version=` in `.csproj`). +- The `CleanArc*` namespace/`.sln` naming is internal project naming, **not** template branding — don't rename it without an explicit request (it touches every file and the EF migrations). diff --git a/server/CleanArcTemplate.sln b/server/CleanArcTemplate.sln new file mode 100644 index 0000000..c8e57d2 --- /dev/null +++ b/server/CleanArcTemplate.sln @@ -0,0 +1,139 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33103.184 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{42CAB060-5D50-4E18-8F85-EBA5EB85B268}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "API", "API", "{0E679B58-1D8A-4F5B-8838-6E4DD9258215}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{0E86739A-769C-4597-84D3-7D53BA1D1E3C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{2373AFFC-1389-4D78-8465-074AB22084AF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{DF0CD4C6-B53D-452D-867E-3E3BD24F883F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{542840FF-B0CC-4F8A-9F6E-1898BE0573D7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.SharedKernel", "src\Shared\CleanArc.SharedKernel\CleanArc.SharedKernel.csproj", "{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.Infrastructure.CrossCutting", "src\Infrastructure\CleanArc.Infrastructure.CrossCutting\CleanArc.Infrastructure.CrossCutting.csproj", "{09E81356-0531-42A0-9F7F-00C495F1226E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.Infrastructure.Identity", "src\Infrastructure\CleanArc.Infrastructure.Identity\CleanArc.Infrastructure.Identity.csproj", "{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.Infrastructure.Persistence", "src\Infrastructure\CleanArc.Infrastructure.Persistence\CleanArc.Infrastructure.Persistence.csproj", "{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.Application", "src\Core\CleanArc.Application\CleanArc.Application.csproj", "{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.Domain", "src\Core\CleanArc.Domain\CleanArc.Domain.csproj", "{DC49CD3F-840E-4634-B9DA-595F160E9499}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.Web.Plugins.Grpc", "src\API\Plugins\CleanArc.Web.Plugins.Grpc\CleanArc.Web.Plugins.Grpc.csproj", "{8F7135E8-68C9-4DA8-AA06-04518EBB403B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.Web.Api", "src\API\CleanArc.Web.Api\CleanArc.Web.Api.csproj", "{BE13FF32-B8D5-4AE7-B173-6CA96040B788}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.WebFramework", "src\API\CleanArc.WebFramework\CleanArc.WebFramework.csproj", "{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{77986571-8153-4120-AD08-36729310A56B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BaseSetup", "BaseSetup", "{34B1F72E-A991-4705-ACC5-08E65E46D26E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArc.Tests.Setup", "src\Tests\CleanArc.Tests.Setup\CleanArc.Tests.Setup.csproj", "{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{45FA88C0-9986-40E5-A2E2-7742302518D2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArc.Test.Infrastructure.Identity", "src\Tests\CleanArc.Test.Infrastructure.Identity\CleanArc.Test.Infrastructure.Identity\CleanArc.Test.Infrastructure.Identity.csproj", "{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArc.Infrastructure.Monitoring", "src\Infrastructure\CleanArc.Infrastructure.Monitoring\CleanArc.Infrastructure.Monitoring.csproj", "{7699705C-2C15-467F-957D-4C5EBE4FD92E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{704FAE1E-F0D2-468E-8B3D-E9E6F323ABE8}" + ProjectSection(SolutionItems) = preProject + Directory.Packages.props = Directory.Packages.props + Dockerfile = Dockerfile + docker-compose.yml = docker-compose.yml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Release|Any CPU.Build.0 = Release|Any CPU + {09E81356-0531-42A0-9F7F-00C495F1226E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09E81356-0531-42A0-9F7F-00C495F1226E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09E81356-0531-42A0-9F7F-00C495F1226E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09E81356-0531-42A0-9F7F-00C495F1226E}.Release|Any CPU.Build.0 = Release|Any CPU + {3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Release|Any CPU.Build.0 = Release|Any CPU + {9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Release|Any CPU.Build.0 = Release|Any CPU + {9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Release|Any CPU.Build.0 = Release|Any CPU + {DC49CD3F-840E-4634-B9DA-595F160E9499}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC49CD3F-840E-4634-B9DA-595F160E9499}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC49CD3F-840E-4634-B9DA-595F160E9499}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC49CD3F-840E-4634-B9DA-595F160E9499}.Release|Any CPU.Build.0 = Release|Any CPU + {8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Release|Any CPU.Build.0 = Release|Any CPU + {BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Release|Any CPU.Build.0 = Release|Any CPU + {44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Release|Any CPU.Build.0 = Release|Any CPU + {33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Release|Any CPU.Build.0 = Release|Any CPU + {54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Release|Any CPU.Build.0 = Release|Any CPU + {7699705C-2C15-467F-957D-4C5EBE4FD92E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7699705C-2C15-467F-957D-4C5EBE4FD92E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7699705C-2C15-467F-957D-4C5EBE4FD92E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7699705C-2C15-467F-957D-4C5EBE4FD92E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {0E679B58-1D8A-4F5B-8838-6E4DD9258215} = {42CAB060-5D50-4E18-8F85-EBA5EB85B268} + {0E86739A-769C-4597-84D3-7D53BA1D1E3C} = {42CAB060-5D50-4E18-8F85-EBA5EB85B268} + {2373AFFC-1389-4D78-8465-074AB22084AF} = {42CAB060-5D50-4E18-8F85-EBA5EB85B268} + {DF0CD4C6-B53D-452D-867E-3E3BD24F883F} = {42CAB060-5D50-4E18-8F85-EBA5EB85B268} + {542840FF-B0CC-4F8A-9F6E-1898BE0573D7} = {0E679B58-1D8A-4F5B-8838-6E4DD9258215} + {56C4DDD2-4F8C-4D35-85D4-CC9064C52398} = {DF0CD4C6-B53D-452D-867E-3E3BD24F883F} + {09E81356-0531-42A0-9F7F-00C495F1226E} = {2373AFFC-1389-4D78-8465-074AB22084AF} + {3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F} = {2373AFFC-1389-4D78-8465-074AB22084AF} + {9F3B3E49-3E3C-4244-AE88-D209B18B28B8} = {2373AFFC-1389-4D78-8465-074AB22084AF} + {9C0BCB6F-614C-4FA9-83A2-E95834E3C153} = {0E86739A-769C-4597-84D3-7D53BA1D1E3C} + {DC49CD3F-840E-4634-B9DA-595F160E9499} = {0E86739A-769C-4597-84D3-7D53BA1D1E3C} + {8F7135E8-68C9-4DA8-AA06-04518EBB403B} = {542840FF-B0CC-4F8A-9F6E-1898BE0573D7} + {BE13FF32-B8D5-4AE7-B173-6CA96040B788} = {0E679B58-1D8A-4F5B-8838-6E4DD9258215} + {44DD0A96-BA65-476E-BC59-C8D2CFA703B9} = {0E679B58-1D8A-4F5B-8838-6E4DD9258215} + {77986571-8153-4120-AD08-36729310A56B} = {42CAB060-5D50-4E18-8F85-EBA5EB85B268} + {34B1F72E-A991-4705-ACC5-08E65E46D26E} = {77986571-8153-4120-AD08-36729310A56B} + {33AF382A-9E22-42F0-82E5-4F78BCFD40C1} = {34B1F72E-A991-4705-ACC5-08E65E46D26E} + {45FA88C0-9986-40E5-A2E2-7742302518D2} = {77986571-8153-4120-AD08-36729310A56B} + {54203B4F-3CE8-4EBA-B5E2-F7C985FACE60} = {45FA88C0-9986-40E5-A2E2-7742302518D2} + {7699705C-2C15-467F-957D-4C5EBE4FD92E} = {2373AFFC-1389-4D78-8465-074AB22084AF} + {704FAE1E-F0D2-468E-8B3D-E9E6F323ABE8} = {42CAB060-5D50-4E18-8F85-EBA5EB85B268} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {05C223B9-EA89-44B2-B9F5-D01181F85DFE} + EndGlobalSection +EndGlobal diff --git a/server/Directory.Packages.props b/server/Directory.Packages.props new file mode 100644 index 0000000..f40084d --- /dev/null +++ b/server/Directory.Packages.props @@ -0,0 +1,57 @@ + + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..1e651bb --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,36 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY ["../Directory.Packages.props", "./"] +COPY ["src/API/CleanArc.Web.Api/CleanArc.Web.Api.csproj", "src/API/CleanArc.Web.Api/"] +COPY ["src/API/CleanArc.WebFramework/CleanArc.WebFramework.csproj", "src/API/CleanArc.WebFramework/"] +COPY ["src/API/Plugins/CleanArc.Web.Plugins.Grpc/CleanArc.Web.Plugins.Grpc.csproj", "src/API/Plugins/CleanArc.Web.Plugins.Grpc/"] +COPY ["src/Core/CleanArc.Application/CleanArc.Application.csproj", "src/Core/CleanArc.Application/"] +COPY ["src/Core/CleanArc.Domain/CleanArc.Domain.csproj", "src/Core/CleanArc.Domain/"] +COPY ["src/Infrastructure/CleanArc.Infrastructure.CrossCutting/CleanArc.Infrastructure.CrossCutting.csproj", "src/Infrastructure/CleanArc.Infrastructure.CrossCutting/"] +COPY ["src/Infrastructure/CleanArc.Infrastructure.Identity/CleanArc.Infrastructure.Identity.csproj", "src/Infrastructure/CleanArc.Infrastructure.Identity/"] +COPY ["src/Infrastructure/CleanArc.Infrastructure.Persistence/CleanArc.Infrastructure.Persistence.csproj", "src/Infrastructure/CleanArc.Infrastructure.Persistence/"] +COPY ["src/Infrastructure/CleanArc.Infrastructure.Monitoring/CleanArc.Infrastructure.Monitoring.csproj", "src/Infrastructure/CleanArc.Infrastructure.Monitoring/"] +COPY ["src/Shared/CleanArc.SharedKernel/CleanArc.SharedKernel.csproj", "src/Shared/CleanArc.SharedKernel/"] +COPY ["src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity.csproj", "src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/"] +COPY ["src/Tests/CleanArc.Tests.Setup/CleanArc.Tests.Setup.csproj", "src/Tests/CleanArc.Tests.Setup/"] + + +RUN dotnet restore "src/API/CleanArc.Web.Api/CleanArc.Web.Api.csproj" +COPY . . +WORKDIR "src/API/CleanArc.Web.Api" +RUN dotnet build "CleanArc.Web.Api.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "CleanArc.Web.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "CleanArc.Web.Api.dll"] diff --git a/server/LICENSE.md b/server/LICENSE.md new file mode 100644 index 0000000..5207cb6 --- /dev/null +++ b/server/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Babak Taremi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..a7433bc --- /dev/null +++ b/server/README.md @@ -0,0 +1,121 @@ +# Balinyaar — Server (ASP.NET Core, Clean Architecture) + +Backend API for the Balinyaar application. It is an **ASP.NET Core (.NET 10)** solution organized around **Clean Architecture**, with: + +- **CQRS** via MediatR (source-generated) +- **ASP.NET Core Identity** with **JWE** (signed + encrypted JWT) and **OTP** authentication +- **Dynamic, permission-based authorization** +- **EF Core** persistence (SQL Server) with the Repository + Unit of Work patterns +- A modular **gRPC plugin** mounted via Application Parts +- Observability out of the box: Serilog, OpenTelemetry, Prometheus metrics, health checks + +> Looking for an architecture/file map to navigate the code? See [AGENTS.md](AGENTS.md). + +## Requirements + +- [.NET 10 SDK](https://dotnet.microsoft.com/) +- SQL Server (local instance, or the containerized one from `docker-compose.yml`) + +## Running locally + +From the `server/` folder: + +```bash +dotnet restore CleanArcTemplate.sln +dotnet build CleanArcTemplate.sln +dotnet run --project src/API/CleanArc.Web.Api/CleanArc.Web.Api.csproj +``` + +By default the API listens on **https://localhost:5002** and serves Swagger UI at **/swagger**. + +On startup the app **applies EF Core migrations** and **seeds default users** automatically, so a reachable database (see `appsettings.json` → `ConnectionStrings`) is required. + +### Configuration + +Settings live in `src/API/CleanArc.Web.Api/appsettings.json` (+ `appsettings.Development.json`): + +- `ConnectionStrings:SqlServer` — main application database +- `ConnectionStrings:logDb` — Serilog SQL sink database +- `IdentitySettings` — `SecretKey` (signing), `Encryptkey` (AES-128 encryption, exactly 16 chars), `Issuer`, `Audience`, token lifetimes + +## Running with Docker + +Generate a development HTTPS certificate (used by the container): + +```bash +dotnet dev-certs https -ep $env:USERPROFILE/.aspnet/https/cleanarc.pfx -p Strong@Password +dotnet dev-certs https --trust +``` + +Build and start the API together with SQL Server 2022: + +```bash +docker build -t balinyaar-server -f Dockerfile . +docker-compose up -d +``` + +The compose stack exposes the API on `http://localhost:5000` / `https://localhost:5001` and SQL Server on `localhost:1435`. + +## Solution layout + +``` +src/ +├── Core/ +│ ├── CleanArc.Domain Entities + domain primitives (the core / "brain") +│ └── CleanArc.Application CQRS features, contracts (interfaces), MediatR pipeline +├── Infrastructure/ +│ ├── CleanArc.Infrastructure.Persistence EF Core DbContext, configs, repositories, UoW, migrations +│ ├── CleanArc.Infrastructure.Identity Identity, JWE/JWT, OTP, dynamic permissions +│ ├── CleanArc.Infrastructure.CrossCutting Cross-cutting concerns (logging) +│ └── CleanArc.Infrastructure.Monitoring Health checks, OpenTelemetry, Prometheus +├── API/ +│ ├── CleanArc.Web.Api Presentation: REST controllers, Program.cs (startup) +│ ├── CleanArc.WebFramework Reusable web config: base controller, filters, middleware, Swagger +│ └── Plugins/CleanArc.Web.Plugins.Grpc Self-contained gRPC plugin +├── Shared/ +│ └── CleanArc.SharedKernel Shared extensions/helpers referenced by every layer +└── Tests/ xUnit test setup + Identity tests +``` + +## The layers (why they exist) + +### Domain + +The core of the project. Each entity may carry its own behavior. Entities derive from a common `BaseEntity`, which lets the persistence layer discover models via reflection to register them and drive migrations. + +### Application + +Routes requests and defines the **contracts** (interfaces) the system depends on, without knowing their implementations. This is where **CQRS** lives: Commands and Queries are kept separate and dispatched to their handlers by **MediatR**. Cross-cutting concerns (validation, metrics) are applied as MediatR pipeline behaviors. + +### Infrastructure + +Implements the contracts the Application layer declares — the parts needed to run in the real world: + +- **Persistence** — the chosen database (SQL Server via EF Core). Repositories give self-describing, persistence-agnostic data access; Unit of Work keeps multi-step writes atomic and consistent. +- **Identity** — registration, authentication and authorization using ASP.NET Core Identity, with JWE tokens, OTP login, and a dynamic access-control system. +- **CrossCutting** — services used across the whole app, such as logging. +- **Monitoring** — health checks, distributed tracing/metrics, and Prometheus. + +### WebFramework + +Keeps `Program.cs` thin by moving each piece of configuration into its own reusable class (filters, middleware, Swagger, API versioning, the base controller, etc.). + +### Web.Api + +The presentation layer — an ASP.NET Core Web API exposing versioned REST controllers under `Controllers/V1`. + +### Web.Plugins.Grpc + +A standalone module that adds gRPC endpoints to the same host via **Application Parts**, giving modularity (the "plugin" middle ground between a monolith and microservices) without a separate deployment. It registers its services and pipeline through extension methods called from `Program.cs`. + +## Tests + +```bash +dotnet test CleanArcTemplate.sln +``` + +Each layer is designed to be testable in isolation; `CleanArc.Tests.Setup` provides the shared test scaffolding. + +## License + +See [LICENSE.md](LICENSE.md). diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 0000000..133fa4a --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,22 @@ +version: "3.9" # optional since v1.27.0 +services: + web_api: + image: bobby-cleanarc + container_name: bobby-cleanarc-app + environment: + "ASPNETCORE_URLS": "https://+;http://+" + "ASPNETCORE_Kestrel__Certificates__Default__Password": "Strong@Password" + "ASPNETCORE_Kestrel__Certificates__Default__Path": "/https/cleanarc.pfx" + ports: + - "5000:80" + - "5001:443" + volumes: + - ~/.aspnet/https:/https + sql: + image: "mcr.microsoft.com/mssql/server:2022-latest" + container_name: sql_server2022 + ports: # not actually needed, because the two services are on the same network + - "1435:1433" + environment: + - ACCEPT_EULA=y + - SA_PASSWORD=A&VeryComplex123Password diff --git a/server/src/API/CleanArc.Web.Api/CleanArc.Web.Api.csproj b/server/src/API/CleanArc.Web.Api/CleanArc.Web.Api.csproj new file mode 100644 index 0000000..9e8f166 --- /dev/null +++ b/server/src/API/CleanArc.Web.Api/CleanArc.Web.Api.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + true + true + $(NoWarn);1591 + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/AdminManagerController.cs b/server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/AdminManagerController.cs new file mode 100644 index 0000000..220fc82 --- /dev/null +++ b/server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/AdminManagerController.cs @@ -0,0 +1,37 @@ +using Asp.Versioning; +using CleanArc.Application.Features.Admin.Commands.AddAdminCommand; +using CleanArc.Application.Features.Admin.Queries.GetToken; +using CleanArc.Application.Models.Jwt; +using CleanArc.WebFramework.Attributes; +using CleanArc.WebFramework.BaseController; +using Mediator; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace CleanArc.Web.Api.Controllers.V1.Admin +{ + [ApiVersion("1")] + [ApiController] + [Route("api/v{version:apiVersion}/AdminManager")] + public class AdminManagerController(ISender sender) : BaseController + { + [HttpPost("Login")] + [ProducesOkApiResponseType] + public async Task AdminLogin(AdminGetTokenQuery model) + { + var query = await sender.Send(model); + + return base.OperationResult(query); + } + + [Authorize(Roles = "admin")] + [HttpPost("NewAdmin")] + [ProducesOkApiResponseType] + public async Task AddNewAdmin(AddAdminCommand model) + { + var commandResult = await sender.Send(model); + + return base.OperationResult(commandResult); + } + } +} diff --git a/server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/OrderManagementController.cs b/server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/OrderManagementController.cs new file mode 100644 index 0000000..8c7d1e9 --- /dev/null +++ b/server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/OrderManagementController.cs @@ -0,0 +1,36 @@ +using CleanArc.Infrastructure.Identity.Identity.PermissionManager; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; +using Asp.Versioning; +using CleanArc.Application.Features.Order.Queries.GetAllOrders; +using CleanArc.WebFramework.Attributes; +using CleanArc.WebFramework.BaseController; +using Mediator; + +namespace CleanArc.Web.Api.Controllers.V1.Admin +{ + [ApiVersion("1")] + [ApiController] + [Route("api/v{version:apiVersion}/OrderManagement")] + [Display(Description= "Managing Users related Orders")] + [Authorize(ConstantPolicies.DynamicPermission)] + public class OrderManagementController : BaseController + { + private readonly ISender _sender; + + public OrderManagementController(ISender sender) + { + _sender = sender; + } + + [HttpGet("OrderList")] + [ProducesOkApiResponseType>] + public async Task GetOrders() + { + var queryResult = await _sender.Send(new GetAllOrdersQuery()); + + return base.OperationResult(queryResult); + } + } +} diff --git a/server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/RoleManagerController.cs b/server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/RoleManagerController.cs new file mode 100644 index 0000000..7903c05 --- /dev/null +++ b/server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/RoleManagerController.cs @@ -0,0 +1,67 @@ +using System.ComponentModel.DataAnnotations; +using Asp.Versioning; +using CleanArc.Application.Features.Role.Commands.AddRoleCommand; +using CleanArc.Application.Features.Role.Commands.UpdateRoleClaimsCommand; +using CleanArc.Application.Features.Role.Queries.GetAllRolesQuery; +using CleanArc.Application.Features.Role.Queries.GetAuthorizableRoutesQuery; +using CleanArc.Infrastructure.Identity.Identity.PermissionManager; +using CleanArc.WebFramework.Attributes; +using CleanArc.WebFramework.BaseController; +using Mediator; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace CleanArc.Web.Api.Controllers.V1.Admin +{ + [ApiVersion("1")] + [ApiController] + [Route("api/v{version:apiVersion}/RoleManager")] + [Authorize(ConstantPolicies.DynamicPermission)] + [Display(Description = "Managing Related Roles for the System")] + + public class RoleManagerController(ISender sender) : BaseController + { + [HttpGet("Roles")] + [ProducesOkApiResponseType>] + public async Task GetRoles() + { + var queryResult = await sender.Send(new GetAllRolesQuery()); + + return base.OperationResult(queryResult); + } + + [HttpGet("AuthRoutes")] + [ProducesOkApiResponseType>] + public async Task GetAuthRoutes() + { + var queryModel = await sender.Send(new GetAuthorizableRoutesQuery()); + + return base.OperationResult(queryModel); + } + + /// + /// Update a role permissions (claims) based on RouteKey received in AuthRoutes API + /// + /// + /// + [HttpPut("UpdateRolePermissions")] + [ProducesOkApiResponseType] + public async Task UpdateRolePermissions(UpdateRoleClaimsCommand model) + { + var commandResult = + await sender.Send(new UpdateRoleClaimsCommand(model.RoleId, model.RoleClaimValue)); + + return base.OperationResult(commandResult); + } + + [HttpPost("NewRole")] + [ProducesOkApiResponseType] + public async Task AddRole(AddRoleCommand model) + { + var commandResult = await sender.Send(model); + + return base.OperationResult(commandResult); + } + + } +} diff --git a/server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/UserManagementController.cs b/server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/UserManagementController.cs new file mode 100644 index 0000000..0cf671c --- /dev/null +++ b/server/src/API/CleanArc.Web.Api/Controllers/V1/Admin/UserManagementController.cs @@ -0,0 +1,36 @@ +using CleanArc.Infrastructure.Identity.Identity.PermissionManager; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; +using Asp.Versioning; +using CleanArc.Application.Features.Users.Queries.GetUsers; +using CleanArc.WebFramework.Attributes; +using CleanArc.WebFramework.BaseController; +using Mediator; + +namespace CleanArc.Web.Api.Controllers.V1.Admin +{ + [ApiVersion("1")] + [ApiController] + [Route("api/v{version:apiVersion}/UserManagement")] + [Display(Description = "Managing API Users")] + [Authorize(ConstantPolicies.DynamicPermission)] + public class UserManagementController : BaseController + { + private readonly ISender _sender; + + public UserManagementController(ISender sender) + { + _sender = sender; + } + + [HttpGet("CurrentUsers")] + [ProducesOkApiResponseType>] + public async Task GetAllUsers() + { + var queryResult = await _sender.Send(new GetUsersQuery()); + + return base.OperationResult(queryResult); + } + } +} diff --git a/server/src/API/CleanArc.Web.Api/Controllers/V1/Order/OrderController.cs b/server/src/API/CleanArc.Web.Api/Controllers/V1/Order/OrderController.cs new file mode 100644 index 0000000..7166427 --- /dev/null +++ b/server/src/API/CleanArc.Web.Api/Controllers/V1/Order/OrderController.cs @@ -0,0 +1,52 @@ +using Asp.Versioning; +using CleanArc.Application.Features.Order.Commands; +using CleanArc.Application.Features.Order.Queries.GetUserOrders; +using CleanArc.WebFramework.Attributes; +using CleanArc.WebFramework.BaseController; +using Mediator; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace CleanArc.Web.Api.Controllers.V1.Order; + +[ApiVersion("1")] +[ApiController] +[Route("api/v{version:apiVersion}/User")] +[Authorize] +public class OrderController(ISender sender) : BaseController +{ + [HttpPost("CreateNewOrder")] + [ProducesOkApiResponseType] + public async Task CreateNewOrder(AddOrderCommand model) + { + model.UserId = base.UserId; + var command = await sender.Send(model); + + return base.OperationResult(command); + } + + [HttpGet("GetUserOrders")] + [ProducesOkApiResponseType>] + public async Task GetUserOrders() + { + var query = await sender.Send(new GetUserOrdersQueryModel(UserId)); + + return base.OperationResult(query); + } + + [HttpPut("UpdateOrder")] + [ProducesOkApiResponseType] + public async Task UpdateOrder(UpdateUserOrderCommand model) + { + model.UserId=base.UserId; + + var command = await sender.Send(model); + + return base.OperationResult(command); + } + + [HttpDelete("DeleteAllUserOrders")] + [ProducesOkApiResponseType] + public async Task DeleteAllUserOrders() + => base.OperationResult(await sender.Send(new DeleteUserOrdersCommand(base.UserId))); +} \ No newline at end of file diff --git a/server/src/API/CleanArc.Web.Api/Controllers/V1/UserManagement/UserController.cs b/server/src/API/CleanArc.Web.Api/Controllers/V1/UserManagement/UserController.cs new file mode 100644 index 0000000..4f2f34f --- /dev/null +++ b/server/src/API/CleanArc.Web.Api/Controllers/V1/UserManagement/UserController.cs @@ -0,0 +1,88 @@ +using Asp.Versioning; +using CleanArc.Application.Features.Users.Commands.Create; +using CleanArc.Application.Features.Users.Commands.RefreshUserTokenCommand; +using CleanArc.Application.Features.Users.Commands.RequestLogout; +using CleanArc.Application.Features.Users.Queries.GenerateUserToken; +using CleanArc.Application.Features.Users.Queries.TokenRequest; +using CleanArc.Application.Models.Jwt; +using CleanArc.WebFramework.Attributes; +using CleanArc.WebFramework.BaseController; +using CleanArc.WebFramework.Swagger; +using Mediator; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace CleanArc.Web.Api.Controllers.V1.UserManagement; + +[ApiVersion("1")] +[ApiController] +[Route("api/v{version:apiVersion}/User")] +public class UserController : BaseController +{ + private readonly IMediator _mediator; + + public UserController(IMediator mediator) + { + _mediator = mediator; + } + + [HttpPost("Register")] + [ProducesOkApiResponseType] + public async Task CreateUser(UserCreateCommand model) + { + var command = await _mediator.Send(model); + + return base.OperationResult(command); + } + + + [HttpPost("TokenRequest")] + [ProducesOkApiResponseType] + public async Task TokenRequest(UserTokenRequestQuery model) + { + var query = await _mediator.Send(model); + + return base.OperationResult(query); + } + + [HttpPost("LoginConfirmation")] + [ProducesOkApiResponseType] + public async Task ValidateUser(GenerateUserTokenQuery model) + { + var result = await _mediator.Send(model); + + return base.OperationResult(result); + } + + [HttpPost("RefreshSignIn")] + [RequireTokenWithoutAuthorization] + [ProducesOkApiResponseType] + public async Task RefreshUserToken(RefreshUserTokenCommand model) + { + var checkCurrentAccessTokenValidity =await HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); + + if (checkCurrentAccessTokenValidity.Succeeded) + return BadRequest("Current access token is valid. No need to refresh"); + + var newTokenResult = await _mediator.Send(model); + + return base.OperationResult(newTokenResult); + } + + [HttpPost("Logout")] + [Authorize] + [ProducesOkApiResponseType] + public async Task RequestLogout() + { + var commandResult = await _mediator.Send(new RequestLogoutCommand(base.UserId)); + + return base.OperationResult(commandResult); + } + + [HttpPost("PasswordTokenRequest")] + [ProducesOkApiResponseType] + public async Task PasswordTokenRequest(PasswordUserTokenRequestQuery model) + => base.OperationResult(await _mediator.Send(model)); +} \ No newline at end of file diff --git a/server/src/API/CleanArc.Web.Api/Program.cs b/server/src/API/CleanArc.Web.Api/Program.cs new file mode 100644 index 0000000..88aaa2c --- /dev/null +++ b/server/src/API/CleanArc.Web.Api/Program.cs @@ -0,0 +1,118 @@ +using System.Diagnostics; +using CleanArc.Application.Features.Users.Commands.Create; +using CleanArc.Application.Models.ApiResult; +using CleanArc.Application.Models.Identity; +using CleanArc.Application.ServiceConfiguration; +using CleanArc.Domain.Entities.User; +using CleanArc.Infrastructure.CrossCutting.Logging; +using CleanArc.Infrastructure.Identity.Identity.Dtos; +using CleanArc.Infrastructure.Identity.Jwt; +using CleanArc.Infrastructure.Identity.ServiceConfiguration; +using CleanArc.Infrastructure.Monitoring.Configurations; +using CleanArc.Infrastructure.Persistence.ServiceConfiguration; +using CleanArc.SharedKernel.Extensions; +using CleanArc.Web.Api.Controllers.V1.UserManagement; +using CleanArc.Web.Plugins.Grpc; +using CleanArc.WebFramework.Filters; +using CleanArc.WebFramework.Middlewares; +using CleanArc.WebFramework.ServiceConfiguration; +using CleanArc.WebFramework.Swagger; +using Mapster; +using Microsoft.AspNetCore.Mvc; +using Serilog; + +var builder = WebApplication.CreateBuilder(args); + +builder.Host.UseSerilog(LoggingConfiguration.ConfigureLogger); + +var configuration = builder.Configuration; + +Activity.DefaultIdFormat = ActivityIdFormat.W3C; + +builder + .ConfigureHealthChecks() + .SetupOpenTelemetry(); + +builder.Services.Configure(configuration.GetSection(nameof(IdentitySettings))); + +var identitySettings = configuration.GetSection(nameof(IdentitySettings)).Get(); + +builder.Services.AddControllers(options => +{ + options.Filters.Add(typeof(OkResultAttribute)); + options.Filters.Add(typeof(NotFoundResultAttribute)); + options.Filters.Add(typeof(ContentResultFilterAttribute)); + options.Filters.Add(typeof(ModelStateValidationAttribute)); + options.Filters.Add(typeof(BadRequestResultFilterAttribute)); + options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult>>), + StatusCodes.Status400BadRequest)); + options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult), + StatusCodes.Status401Unauthorized)); + options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult), + StatusCodes.Status403Forbidden)); + options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult), + StatusCodes.Status500InternalServerError)); + +}).ConfigureApiBehaviorOptions(options => +{ + options.SuppressModelStateInvalidFilter = true; + options.SuppressMapClientErrors = true; +}); + + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwagger("v1","v1.1"); + + +builder.Services.AddApplicationServices() + .RegisterIdentityServices(identitySettings) + .AddPersistenceServices(configuration) + .AddWebFrameworkServices(); + +builder.Services.RegisterValidatorsAsServices(); +builder.Services.AddExceptionHandler(); + +builder.Services.AddMapster(); + +TypeAdapterConfig.GlobalSettings.Scan(typeof(UserCreateCommand).Assembly, + typeof(GetRolesDto).Assembly); + + +#region Plugin Services Configuration + +builder.Services.ConfigureGrpcPluginServices(); + +#endregion + + + +var app = builder.Build(); + + +await app.ApplyMigrationsAsync(); +await app.SeedDefaultUsersAsync(); + +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} +else + app.UseExceptionHandler(_=>{}); + +app.UseSwaggerAndUi(); + +app.UseRouting(); + +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); + +app.UseMetrics() + .UseHealthChecks(); + +app.ConfigureGrpcPipeline(); + +await app.RunAsync(); + + + diff --git a/server/src/API/CleanArc.Web.Api/Properties/launchSettings.json b/server/src/API/CleanArc.Web.Api/Properties/launchSettings.json new file mode 100644 index 0000000..a5a3d3a --- /dev/null +++ b/server/src/API/CleanArc.Web.Api/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:8746", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Web.Api": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5002", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/server/src/API/CleanArc.Web.Api/appsettings.Development.json b/server/src/API/CleanArc.Web.Api/appsettings.Development.json new file mode 100644 index 0000000..e3ce936 --- /dev/null +++ b/server/src/API/CleanArc.Web.Api/appsettings.Development.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "SqlServer": "Data Source=localhost;Initial Catalog=CleanArc_DB_8_0;Integrated Security=true;Encrypt=False" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/server/src/API/CleanArc.Web.Api/appsettings.json b/server/src/API/CleanArc.Web.Api/appsettings.json new file mode 100644 index 0000000..4785216 --- /dev/null +++ b/server/src/API/CleanArc.Web.Api/appsettings.json @@ -0,0 +1,20 @@ +{ + "ConnectionStrings": { + "SqlServer": "Server=sql_server2022;Database=CleanArc_DB_Docker;User Id=SA;Password=A&VeryComplex123Password;MultipleActiveResultSets=true;encrypt=false", + "logDb": "Server=sql_server2022;Database=CleanArc_Log_DB_Docker;User Id=SA;Password=A&VeryComplex123Password;MultipleActiveResultSets=true;encrypt=false" + }, + "IdentitySettings": { + "SecretKey": "ShouldBe-LongerThan-16Char-SecretKey", + "Encryptkey": "16CharEncryptKey", + "Issuer": "MyWebsite", + "Audience": "MyWebsite", + "NotBeforeMinutes": "0", + "ExpirationMinutes": "10000" + }, + "AllowedHosts": "*", + "Kestrel": { + "EndpointDefaults": { + "Protocols": "Http2" + } + } +} diff --git a/server/src/API/CleanArc.WebFramework/Attributes/ProducesOkApiResponseTypeAttribute.cs b/server/src/API/CleanArc.WebFramework/Attributes/ProducesOkApiResponseTypeAttribute.cs new file mode 100644 index 0000000..0df1d56 --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/Attributes/ProducesOkApiResponseTypeAttribute.cs @@ -0,0 +1,39 @@ +using CleanArc.Application.Models.ApiResult; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace CleanArc.WebFramework.Attributes; + +/// +/// Documents the 200 OK response type of endpoint as ApiResult +/// +/// API response type that will be in data JSON property +public class ProducesOkApiResponseType:ProducesResponseTypeAttribute +{ + private ProducesOkApiResponseType(int statusCode) : base(statusCode) + { + } + + public ProducesOkApiResponseType() : base(typeof(ApiResult), StatusCodes.Status200OK) + { + } + + private ProducesOkApiResponseType(Type type, int statusCode, string contentType, params string[] additionalContentTypes) : base(type, statusCode, contentType, additionalContentTypes) + { + } +} + +public class ProducesOkApiResponseType:ProducesResponseTypeAttribute +{ + private ProducesOkApiResponseType(int statusCode) : base(statusCode) + { + } + + public ProducesOkApiResponseType() : base(typeof(ApiResult), StatusCodes.Status200OK) + { + } + + private ProducesOkApiResponseType(Type type, int statusCode, string contentType, params string[] additionalContentTypes) : base(type, statusCode, contentType, additionalContentTypes) + { + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/BaseController/BaseController.cs b/server/src/API/CleanArc.WebFramework/BaseController/BaseController.cs new file mode 100644 index 0000000..6f9623c --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/BaseController/BaseController.cs @@ -0,0 +1,52 @@ +using System.Security.Claims; +using CleanArc.Application.Models.Common; +using CleanArc.SharedKernel.Extensions; +using CleanArc.WebFramework.Filters; +using Microsoft.AspNetCore.Mvc; + +namespace CleanArc.WebFramework.BaseController; + +public class BaseController : ControllerBase +{ + protected string UserName => User.Identity?.Name; + protected int UserId => int.Parse(User.Identity.GetUserId()); + protected string UserEmail => User.Identity.FindFirstValue(ClaimTypes.Email); + protected string UserRole => User.Identity.FindFirstValue(ClaimTypes.Role); + + protected string UserKey => User.FindFirstValue(ClaimTypes.UserData); + + protected IActionResult OperationResult(OperationResult result) + { + if (result is null) + return new ServerErrorResult("Server Error"); + + + if (result.IsSuccess) + return result.Result is bool ? Ok() : Ok(result.Result); + + if (result.IsNotFound) + { + + AddErrors(result); + + var notFoundErrors = new ValidationProblemDetails(ModelState); + + return NotFound(notFoundErrors.Errors); + } + + AddErrors(result); + + var badRequestErrors = new ValidationProblemDetails(ModelState); + + return BadRequest(badRequestErrors.Errors); + + } + + private void AddErrors(OperationResult result) + { + foreach (var error in result.ErrorMessages) + { + ModelState.AddModelError(error.Key,error.Value); + } + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/CleanArc.WebFramework.csproj b/server/src/API/CleanArc.WebFramework/CleanArc.WebFramework.csproj new file mode 100644 index 0000000..a95c19b --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/CleanArc.WebFramework.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + + + + + + + + + + + + + + + + + + diff --git a/server/src/API/CleanArc.WebFramework/EndpointFilters/BadRequestResultEndpointFilter.cs b/server/src/API/CleanArc.WebFramework/EndpointFilters/BadRequestResultEndpointFilter.cs new file mode 100644 index 0000000..ecf8218 --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/EndpointFilters/BadRequestResultEndpointFilter.cs @@ -0,0 +1,28 @@ +using CleanArc.Application.Models.ApiResult; +using Microsoft.AspNetCore.Http; + +namespace CleanArc.WebFramework.EndpointFilters; + +public class BadRequestResultEndpointFilter:IEndpointFilter +{ + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + var result = await next(context); + + if (result is not IStatusCodeHttpResult statusCodeResult) + { + return result; + } + + if (statusCodeResult.StatusCode != StatusCodes.Status400BadRequest) + return result; + + + if (result is IValueHttpResult valueHttp) + { + return Results.BadRequest(new ApiResult(false, ApiResultStatusCode.BadRequest, valueHttp.Value)); + } + + return Results.BadRequest(new ApiResult(false, ApiResultStatusCode.BadRequest)); + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/EndpointFilters/ModelStateValidationEndpointFilter.cs b/server/src/API/CleanArc.WebFramework/EndpointFilters/ModelStateValidationEndpointFilter.cs new file mode 100644 index 0000000..7ea13e5 --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/EndpointFilters/ModelStateValidationEndpointFilter.cs @@ -0,0 +1,46 @@ +using FluentValidation; +using Microsoft.AspNetCore.Http; + +namespace CleanArc.WebFramework.EndpointFilters; + +public class ModelStateValidationEndpointFilter:IEndpointFilter +{ + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + + var validationSummery = new Dictionary>(); + + foreach (var contextArgument in context.Arguments) + { + if (contextArgument is null) + continue; + + var validator = + context.HttpContext.RequestServices.GetService( + typeof(IValidator<>).MakeGenericType(contextArgument.GetType())) as IValidator; + + if (validator is null) + continue; + + var validationResult = await validator.ValidateAsync(new ValidationContext(contextArgument)); + + if (validationResult.IsValid) continue; + + foreach (var validationResultError in validationResult.Errors) + { + if (validationSummery.TryGetValue(validationResultError.PropertyName, out var value)) + { + value.Add(validationResultError.ErrorMessage); + continue; + } + + validationSummery.Add(validationResultError.PropertyName, new (){validationResultError.ErrorMessage}); + } + } + + if (validationSummery.Count == 0) + return await next(context); + + return Results.BadRequest(validationSummery); + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/EndpointFilters/NotFoundResultEndpointFilter.cs b/server/src/API/CleanArc.WebFramework/EndpointFilters/NotFoundResultEndpointFilter.cs new file mode 100644 index 0000000..6ae2244 --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/EndpointFilters/NotFoundResultEndpointFilter.cs @@ -0,0 +1,28 @@ +using CleanArc.Application.Models.ApiResult; +using Microsoft.AspNetCore.Http; + +namespace CleanArc.WebFramework.EndpointFilters; + +public class NotFoundResultEndpointFilter:IEndpointFilter +{ + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + var result = await next(context); + + if (result is not IStatusCodeHttpResult statusCodeResult) + { + return result; + } + + if (statusCodeResult.StatusCode != StatusCodes.Status404NotFound) + return result; + + + if (result is IValueHttpResult valueHttp) + { + return Results.BadRequest(new ApiResult(false, ApiResultStatusCode.NotFound, valueHttp.Value)); + } + + return Results.BadRequest(new ApiResult(false, ApiResultStatusCode.NotFound)); + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/EndpointFilters/OkResultEndpointFilter.cs b/server/src/API/CleanArc.WebFramework/EndpointFilters/OkResultEndpointFilter.cs new file mode 100644 index 0000000..2161a67 --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/EndpointFilters/OkResultEndpointFilter.cs @@ -0,0 +1,28 @@ +using CleanArc.Application.Models.ApiResult; +using Microsoft.AspNetCore.Http; + +namespace CleanArc.WebFramework.EndpointFilters; + +public class OkResultEndpointFilter:IEndpointFilter +{ + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + var result=await next(context); + + if (result is not IStatusCodeHttpResult statusCodeResult) + { + return result; + } + + if(statusCodeResult.StatusCode !=StatusCodes.Status200OK) + return result; + + + if (result is IValueHttpResult valueHttp) + { + return Results.Ok(new ApiResult(true, ApiResultStatusCode.Success, valueHttp.Value)); + } + + return Results.Ok(new ApiResult(true, ApiResultStatusCode.Success)); + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/Filters/ApiResultFilterAttribute.cs b/server/src/API/CleanArc.WebFramework/Filters/ApiResultFilterAttribute.cs new file mode 100644 index 0000000..bcef567 --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/Filters/ApiResultFilterAttribute.cs @@ -0,0 +1,65 @@ +using CleanArc.Application.Models.ApiResult; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace CleanArc.WebFramework.Filters; + +[Obsolete(message:"Separated filters added")] +public class ApiResultFilterAttribute : ResultFilterAttribute +{ + public override void OnResultExecuting(ResultExecutingContext context) + { + if (context.Result is OkObjectResult okObjectResult) + { + var apiResult = new ApiResult(true, ApiResultStatusCode.Success, okObjectResult.Value); + context.Result = new JsonResult(apiResult) { StatusCode = okObjectResult.StatusCode }; + } + else if (context.Result is OkResult okResult) + { + var apiResult = new ApiResult(true, ApiResultStatusCode.Success); + context.Result = new JsonResult(apiResult) { StatusCode = okResult.StatusCode }; + } + else if (context.Result is BadRequestResult badRequestResult) + { + var apiResult = new ApiResult(false, ApiResultStatusCode.BadRequest); + context.Result = new JsonResult(apiResult) { StatusCode = badRequestResult.StatusCode }; + context.HttpContext.Response.StatusCode = 400; + } + else if (context.Result is BadRequestObjectResult badRequestObjectResult) + { + var message = badRequestObjectResult.Value.ToString(); + if (badRequestObjectResult.Value is SerializableError errors) + { + var errorMessages = errors.SelectMany(p => (string[])p.Value).Distinct(); + message = string.Join(" | ", errorMessages); + } + var apiResult = new ApiResult(false, ApiResultStatusCode.BadRequest, message); + context.Result = new JsonResult(apiResult) { StatusCode = badRequestObjectResult.StatusCode }; + context.HttpContext.Response.StatusCode = 400; + } + else if (context.Result is ContentResult contentResult) + { + var apiResult = new ApiResult(true, ApiResultStatusCode.Success, contentResult.Content); + context.Result = new JsonResult(apiResult) { StatusCode = contentResult.StatusCode }; + } + else if (context.Result is NotFoundResultAttribute notFoundResult) + { + var apiResult = new ApiResult(false, ApiResultStatusCode.NotFound); + context.Result = new JsonResult(apiResult) { StatusCode = StatusCodes.Status404NotFound }; + } + else if (context.Result is NotFoundObjectResult notFoundObjectResult) + { + var apiResult = new ApiResult(false, ApiResultStatusCode.NotFound, notFoundObjectResult.Value); + context.Result = new JsonResult(apiResult) { StatusCode = notFoundObjectResult.StatusCode }; + } + else if (context.Result is ObjectResult objectResult && objectResult.StatusCode == null + && !(objectResult.Value is ApiResult)) + { + var apiResult = new ApiResult(true, ApiResultStatusCode.Success, objectResult.Value); + context.Result = new JsonResult(apiResult) { StatusCode = objectResult.StatusCode }; + } + + base.OnResultExecuting(context); + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/Filters/BadRequestResultFilterAttribute.cs b/server/src/API/CleanArc.WebFramework/Filters/BadRequestResultFilterAttribute.cs new file mode 100644 index 0000000..cd94102 --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/Filters/BadRequestResultFilterAttribute.cs @@ -0,0 +1,37 @@ +using CleanArc.Application.Models.ApiResult; +using CleanArc.SharedKernel.Extensions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace CleanArc.WebFramework.Filters; + +public class BadRequestResultFilterAttribute : ActionFilterAttribute +{ + public override void OnResultExecuting(ResultExecutingContext context) + { + if (!(context.Result is BadRequestObjectResult badRequestObjectResult)) return; + + var modelState = context.ModelState; + + if (!modelState.IsValid) + { + var errors = new ValidationProblemDetails(modelState); + + var message = ApiResultStatusCode.BadRequest.ToDisplay(); + + var apiResult = new ApiResult>(false, ApiResultStatusCode.BadRequest, errors.Errors, message); + context.Result = new JsonResult(apiResult) { StatusCode = badRequestObjectResult.StatusCode }; + context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + else + { + + + var apiResult = new ApiResult(false, ApiResultStatusCode.BadRequest,badRequestObjectResult.Value,ApiResultStatusCode.BadRequest.ToDisplay()); + context.Result = new JsonResult(apiResult) { StatusCode = badRequestObjectResult.StatusCode }; + context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + } + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/Filters/ContentResultFilterAttribute.cs b/server/src/API/CleanArc.WebFramework/Filters/ContentResultFilterAttribute.cs new file mode 100644 index 0000000..c01df10 --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/Filters/ContentResultFilterAttribute.cs @@ -0,0 +1,15 @@ +using CleanArc.Application.Models.ApiResult; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace CleanArc.WebFramework.Filters; + +public class ContentResultFilterAttribute : ResultFilterAttribute +{ + public override void OnResultExecuting(ResultExecutingContext context) + { + if (!(context.Result is ContentResult contentResult)) return; + var apiResult = new ApiResult(true, ApiResultStatusCode.Success, contentResult.Content); + context.Result = new JsonResult(apiResult) { StatusCode = contentResult.StatusCode }; + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/Filters/ModelStateValidationAttribute.cs b/server/src/API/CleanArc.WebFramework/Filters/ModelStateValidationAttribute.cs new file mode 100644 index 0000000..60185a6 --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/Filters/ModelStateValidationAttribute.cs @@ -0,0 +1,62 @@ +using CleanArc.Application.Models.ApiResult; +using CleanArc.SharedKernel.Extensions; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using StatusCodes = Microsoft.AspNetCore.Http.StatusCodes; + +namespace CleanArc.WebFramework.Filters; + +public class ModelStateValidationAttribute : ActionFilterAttribute +{ + public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + foreach (var contextActionArgument in context.ActionArguments.Values) + { + var viewModelValidator = + context.HttpContext.RequestServices.GetService( + typeof(IValidator<>).MakeGenericType(contextActionArgument.GetType())); + + if (viewModelValidator is IValidator validator) + { + var validationResult =await validator.ValidateAsync(new ValidationContext(contextActionArgument)); + + if (!validationResult.IsValid) + { + foreach (var validationResultError in validationResult.Errors) + { + context.ModelState.AddModelError(validationResultError.PropertyName, validationResultError.ErrorMessage); + } + } + } + } + + var modelState = context.ModelState; + + if (!modelState.IsValid) + { + + var model = context.ActionArguments.FirstOrDefault().Value; + + if (model != null) + { + var errors = new ValidationProblemDetails(modelState); + + var message = ApiResultStatusCode.BadRequest.ToDisplay(); + + var apiResult = new ApiResult>(false, ApiResultStatusCode.BadRequest, errors.Errors, message); + context.Result = new JsonResult(apiResult) { StatusCode = StatusCodes.Status400BadRequest }; + context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + } + + else + { + var apiResult = new ApiResult(false, ApiResultStatusCode.BadRequest); + context.Result = new JsonResult(apiResult) { StatusCode = 400 }; + context.HttpContext.Response.StatusCode = 400; + } + } + + await base.OnActionExecutionAsync(context, next); + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/Filters/NotFoundResultAttribute.cs b/server/src/API/CleanArc.WebFramework/Filters/NotFoundResultAttribute.cs new file mode 100644 index 0000000..18e2d5c --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/Filters/NotFoundResultAttribute.cs @@ -0,0 +1,24 @@ +using CleanArc.Application.Models.ApiResult; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace CleanArc.WebFramework.Filters; + +public class NotFoundResultAttribute : ResultFilterAttribute +{ + public override void OnResultExecuting(ResultExecutingContext context) + { + if ((context.Result is NotFoundObjectResult notFoundObjectResult)) + { + var apiResult = new ApiResult(false, ApiResultStatusCode.NotFound, notFoundObjectResult.Value); + context.Result = new JsonResult(apiResult) { StatusCode = notFoundObjectResult.StatusCode }; + } + + else if(context.Result is NotFoundResult) + { + var apiResult = new ApiResult(false, ApiResultStatusCode.NotFound); + context.Result = new JsonResult(apiResult) { StatusCode =StatusCodes.Status404NotFound }; + } + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/Filters/OkResultAttribute.cs b/server/src/API/CleanArc.WebFramework/Filters/OkResultAttribute.cs new file mode 100644 index 0000000..c5765f6 --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/Filters/OkResultAttribute.cs @@ -0,0 +1,28 @@ +using CleanArc.Application.Models.ApiResult; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace CleanArc.WebFramework.Filters; + +public class OkResultAttribute:ResultFilterAttribute +{ + public override void OnResultExecuting(ResultExecutingContext context) + { + switch (context.Result) + { + case OkObjectResult okObjectResult: + { + var apiResult = new ApiResult(true, ApiResultStatusCode.Success, okObjectResult.Value); + context.Result = new JsonResult(apiResult) { StatusCode = okObjectResult.StatusCode }; + break; + } + case OkResult okResult: + { + var apiResult = new ApiResult(true, ApiResultStatusCode.Success); + context.Result = new JsonResult(apiResult) { StatusCode = okResult.StatusCode }; + break; + } + default:return; + } + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/Filters/ServerErrorResult.cs b/server/src/API/CleanArc.WebFramework/Filters/ServerErrorResult.cs new file mode 100644 index 0000000..101aeb7 --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/Filters/ServerErrorResult.cs @@ -0,0 +1,22 @@ +using CleanArc.Application.Models.ApiResult; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace CleanArc.WebFramework.Filters; + +public class ServerErrorResult:IActionResult +{ + public string Message { get;} + + public ServerErrorResult(string message) + { + Message = message; + } + + public async Task ExecuteResultAsync(ActionContext context) + { + context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; + var response = new ApiResult(false, ApiResultStatusCode.ServerError, Message); + await context.HttpContext.Response.WriteAsJsonAsync(response); + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/Middlewares/ExceptionHandler.cs b/server/src/API/CleanArc.WebFramework/Middlewares/ExceptionHandler.cs new file mode 100644 index 0000000..32ab6ff --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/Middlewares/ExceptionHandler.cs @@ -0,0 +1,63 @@ +using CleanArc.Application.Models.ApiResult; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using CleanArc.SharedKernel.Extensions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace CleanArc.WebFramework.Middlewares; + +public class ExceptionHandler(ILogger logger,IWebHostEnvironment environment) : IExceptionHandler +{ + + public async ValueTask TryHandleAsync(HttpContext context, Exception exception, CancellationToken cancellationToken) + { + if (environment.IsDevelopment()) + return false; + + if (exception is FluentValidation.ValidationException validationException) + { + context.Response.StatusCode = StatusCodes.Status422UnprocessableEntity; + + var errors = new Dictionary>(); + + foreach (var validationExceptionError in validationException.Errors) + { + if (!errors.ContainsKey(validationExceptionError.PropertyName)) + errors.Add(validationExceptionError.PropertyName, new List() { validationExceptionError.ErrorMessage }); + else + errors[validationExceptionError.PropertyName].Add(validationExceptionError.ErrorMessage); + + } + + var apiResult = new ApiResult>>(false, ApiResultStatusCode.EntityProcessError, errors, ApiResultStatusCode.EntityProcessError.ToDisplay()); + + context.Response.ContentType = "application/problem+json"; + await context.Response.WriteAsJsonAsync(apiResult, cancellationToken: cancellationToken); + + return true; + } + + var exceptionFeature = context.Features.Get(); + + if (exceptionFeature is not null) + logger.LogError(exceptionFeature.Error, + "Unhandled exception occured. Path: {exceptionUrlPath} ." + , exceptionFeature.Path + ); + + else + logger.LogError(exception, "Error captured in global exception handler"); + + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + + context.Response.ContentType = "application/problem+json"; + var response = new ApiResult(false, + ApiResultStatusCode.ServerError, "Internal Server Error"); + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + await context.Response.WriteAsJsonAsync(response, cancellationToken: cancellationToken); + + return true; + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/ServiceConfiguration/ServiceCollectionExtension.cs b/server/src/API/CleanArc.WebFramework/ServiceConfiguration/ServiceCollectionExtension.cs new file mode 100644 index 0000000..d4cd52b --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/ServiceConfiguration/ServiceCollectionExtension.cs @@ -0,0 +1,45 @@ +using Asp.Versioning; +using Microsoft.Extensions.DependencyInjection; + +namespace CleanArc.WebFramework.ServiceConfiguration; + +public static class ServiceCollectionExtension +{ + public static IServiceCollection AddWebFrameworkServices(this IServiceCollection services) + { + services.AddApiVersioning(options => + { + //url segment => {version} + options.AssumeDefaultVersionWhenUnspecified = true; //default => false; + options.DefaultApiVersion = new ApiVersion(1, 0); //v1.0 == v1 + options.ReportApiVersions = true; + + //ApiVersion.TryParse("1.0", out var version10); + //ApiVersion.TryParse("1", out var version1); + //var a = version10 == version1; + + //options.ApiVersionReader = new QueryStringApiVersionReader("api-version"); + // api/posts?api-version=1 + + //options.ApiVersionReader = new UrlSegmentApiVersionReader(); + // api/v1/posts + + //options.ApiVersionReader = new HeaderApiVersionReader(new[] { "Api-Version" }); + // header => Api-Version : 1 + + //options.ApiVersionReader = new MediaTypeApiVersionReader() + + //options.ApiVersionReader = ApiVersionReader.Combine(new QueryStringApiVersionReader("api-version"), new UrlSegmentApiVersionReader()) + // combine of [querystring] & [urlsegment] + }).AddMvc() + .AddApiExplorer(options => + { + options.GroupNameFormat = "'v'V"; + options.SubstituteApiVersionInUrl = true; + });; + + return services; + + + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/Swagger/ApiVersionDocumentProcessor.cs b/server/src/API/CleanArc.WebFramework/Swagger/ApiVersionDocumentProcessor.cs new file mode 100644 index 0000000..af75bba --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/Swagger/ApiVersionDocumentProcessor.cs @@ -0,0 +1,25 @@ +using CleanArc.SharedKernel.Extensions; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; + +namespace CleanArc.WebFramework.Swagger; + +public class ApiVersionDocumentProcessor: IDocumentProcessor +{ + public void Process(DocumentProcessorContext context) + { + // Filter out operations that do not match the current document version + var version = context.Document.Info.Version; // e.g., "v1" + + var pathsToRemove = context.Document.Paths + .Where(pathItem => !RegExHelpers.MatchesApiVersion( version,pathItem.Key)) + .Select(path => path.Key) + .ToList(); + + + foreach (var path in pathsToRemove) + { + context.Document.Paths.Remove(path); + } + } +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/Swagger/ApplySummariesOperationFilter.cs b/server/src/API/CleanArc.WebFramework/Swagger/ApplySummariesOperationFilter.cs new file mode 100644 index 0000000..e910caa --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/Swagger/ApplySummariesOperationFilter.cs @@ -0,0 +1,122 @@ +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; +using Pluralize.NET; + + +namespace CleanArc.WebFramework.Swagger; + +public class ApplySummariesOperationFilter : IOperationProcessor +{ + + + public bool Process(OperationProcessorContext context) + { + + if (context.ControllerType is null) + return true; + + var actionName = context.MethodInfo.Name; + var controllerName = context.ControllerType.Name.Replace("Controller", ""); + + var pluralizer = new Pluralizer(); + + var singularizeName =pluralizer.Singularize(controllerName); + var pluralizeName = pluralizer.Pluralize(singularizeName); + + var parameterCount = context.OperationDescription.Operation.Parameters + .Count(p => p.Name != "version" && p.Name != "api-version"); + + if (IsGetAllAction(actionName, pluralizeName, singularizeName, parameterCount)) + { + if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary)) + { + context.OperationDescription.Operation.Summary = $"Returns all {pluralizeName}"; + } + } + else if (IsActionName(actionName, singularizeName, "Post", "Create")) + { + if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary)) + { + context.OperationDescription.Operation.Summary = $"Creates a {singularizeName}"; + } + + if (context.OperationDescription.Operation.Parameters.Count > 0 && + string.IsNullOrEmpty(context.OperationDescription.Operation.Parameters[0].Description)) + { + context.OperationDescription.Operation.Parameters[0].Description = $"A {singularizeName} representation"; + } + } + else if (IsActionName(actionName, singularizeName, "Read", "Get")) + { + if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary)) + { + context.OperationDescription.Operation.Summary = $"Retrieves a {singularizeName} by unique id"; + } + + if (context.OperationDescription.Operation.Parameters.Count > 0 && + string.IsNullOrEmpty(context.OperationDescription.Operation.Parameters[0].Description)) + { + context.OperationDescription.Operation.Parameters[0].Description = $"A unique id for the {singularizeName}"; + } + } + else if (IsActionName(actionName, singularizeName, "Put", "Edit", "Update")) + { + if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary)) + { + context.OperationDescription.Operation.Summary = $"Updates a {singularizeName} by unique id"; + } + + if (context.OperationDescription.Operation.Parameters.Count > 0 && + string.IsNullOrEmpty(context.OperationDescription.Operation.Parameters[0].Description)) + { + context.OperationDescription.Operation.Parameters[0].Description = $"A {singularizeName} representation"; + } + } + else if (IsActionName(actionName, singularizeName, "Delete", "Remove")) + { + if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary)) + { + context.OperationDescription.Operation.Summary = $"Deletes a {singularizeName} by unique id"; + } + + if (context.OperationDescription.Operation.Parameters.Count > 0 && + string.IsNullOrEmpty(context.OperationDescription.Operation.Parameters[0].Description)) + { + context.OperationDescription.Operation.Parameters[0].Description = $"A unique id for the {singularizeName}"; + } + } + + return true; + } + private bool IsActionName(string actionName, string singularizeName, params string[] names) + { + foreach (var name in names) + { + if (actionName.Equals(name, StringComparison.OrdinalIgnoreCase) || + actionName.Equals($"{name}ById", StringComparison.OrdinalIgnoreCase) || + actionName.Equals($"{name}{singularizeName}", StringComparison.OrdinalIgnoreCase) || + actionName.Equals($"{name}{singularizeName}ById", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + private bool IsGetAllAction(string actionName, string pluralizeName, string singularizeName, int parameterCount) + { + var actionNames = new[] { "Get", "Read", "Select" }; + foreach (var name in actionNames) + { + if ((actionName.Equals(name, StringComparison.OrdinalIgnoreCase) && parameterCount == 0) || + actionName.Equals($"{name}All", StringComparison.OrdinalIgnoreCase) || + actionName.Equals($"{name}{pluralizeName}", StringComparison.OrdinalIgnoreCase) || + actionName.Equals($"{name}All{singularizeName}", StringComparison.OrdinalIgnoreCase) || + actionName.Equals($"{name}All{pluralizeName}", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + +} \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/Swagger/CustomTokenRequiredOperationFilter.cs b/server/src/API/CleanArc.WebFramework/Swagger/CustomTokenRequiredOperationFilter.cs new file mode 100644 index 0000000..b7edbb8 --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/Swagger/CustomTokenRequiredOperationFilter.cs @@ -0,0 +1,36 @@ +using NSwag; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; + + +namespace CleanArc.WebFramework.Swagger; + +public class CustomTokenRequiredOperationFilter : IOperationProcessor +{ + + public bool Process(OperationProcessorContext context) + { + var hasAttribute = context.MethodInfo + .GetCustomAttributes(typeof(RequireTokenWithoutAuthorizationAttribute), false).Any(); + + if (hasAttribute) + { + // Add security requirements to the operation + var securityScheme = new OpenApiSecurityScheme + { + Type = OpenApiSecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT", + In = OpenApiSecurityApiKeyLocation.Header, + Name = "Authorization" + }; + + var securityRequirement = new OpenApiSecurityRequirement { { securityScheme.Scheme, new List() } }; + + + context.OperationDescription.Operation.Security=[securityRequirement]; + } + + return true; + } +} diff --git a/server/src/API/CleanArc.WebFramework/Swagger/RequireTokenWithoutAuthorizationAttribute.cs b/server/src/API/CleanArc.WebFramework/Swagger/RequireTokenWithoutAuthorizationAttribute.cs new file mode 100644 index 0000000..6f7be7c --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/Swagger/RequireTokenWithoutAuthorizationAttribute.cs @@ -0,0 +1,11 @@ +namespace CleanArc.WebFramework.Swagger; + + +/// +/// Marker Attribute for Custom Actions or controllers that need token but without authorization check +/// +[AttributeUsage(AttributeTargets.Class| AttributeTargets.Method)] +public class RequireTokenWithoutAuthorizationAttribute : Attribute +{ + +}; \ No newline at end of file diff --git a/server/src/API/CleanArc.WebFramework/Swagger/SwaggerConfigurationExtensions.cs b/server/src/API/CleanArc.WebFramework/Swagger/SwaggerConfigurationExtensions.cs new file mode 100644 index 0000000..e0658b7 --- /dev/null +++ b/server/src/API/CleanArc.WebFramework/Swagger/SwaggerConfigurationExtensions.cs @@ -0,0 +1,69 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NSwag; + +namespace CleanArc.WebFramework.Swagger; + +public static class SwaggerConfigurationExtensions +{ + public static void AddSwagger(this IServiceCollection services, + params string[] versions) + { + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + + foreach (var version in versions) + { + services.AddOpenApiDocument(options => + { + options.Title = "Clean Architecture OpenAPI docs"; + options.Version = version; + options.DocumentName = version; + + + options.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme() + { + Description = "Enter JWT Token ONLY", + In = OpenApiSecurityApiKeyLocation.Header, + Name = "Authorization", + Type = OpenApiSecuritySchemeType.Http, + Scheme = JwtBearerDefaults.AuthenticationScheme, + }); + + options.DocumentProcessors.Add(new ApiVersionDocumentProcessor()); + options.OperationProcessors.Add(new ApplySummariesOperationFilter()); + options.OperationProcessors.Add(new CustomTokenRequiredOperationFilter()); + + options.OperationProcessors.Add( + new NSwag.Generation.Processors.Security.AspNetCoreOperationSecurityScopeProcessor("Bearer")); + + + }); + } + } + + public static void UseSwaggerAndUi(this WebApplication app) + { + ArgumentNullException.ThrowIfNull(app, nameof(app)); + + app.UseOpenApi(); + + app.UseSwaggerUi(options => + { + options.PersistAuthorization = true; + + options.EnableTryItOut = true; + + options.Path = "/swagger"; + + }); + + app.UseReDoc(settings => + { + settings.Path = "/api-docs/{documentName}"; + settings.DocumentTitle = "Clean Architecture API doc sample"; + }); + } +} \ No newline at end of file diff --git a/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/CleanArc.Web.Plugins.Grpc.csproj b/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/CleanArc.Web.Plugins.Grpc.csproj new file mode 100644 index 0000000..7329f1d --- /dev/null +++ b/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/CleanArc.Web.Plugins.Grpc.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + + + + + + + + + + + + + + + + + + + + + diff --git a/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/GrpcPluginStartup.cs b/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/GrpcPluginStartup.cs new file mode 100644 index 0000000..45ce33b --- /dev/null +++ b/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/GrpcPluginStartup.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using CleanArc.Web.Plugins.Grpc.Services; + +namespace CleanArc.Web.Plugins.Grpc; + +public static class GrpcPluginStartup +{ + public static IServiceCollection ConfigureGrpcPluginServices(this IServiceCollection services) + { + + + services.AddGrpc(); + services.AddGrpcReflection(); + + return services; + } + + public static void ConfigureGrpcPipeline(this WebApplication app) + { + + app.MapGrpcService(); + app.MapGrpcService(); + app.MapGrpcReflectionService(); + + app.MapGet("/GrpcUser", async context => + { + await context.Response.WriteAsync( + "Communication with this gRPC endpoint must be made through a gRPC client."); + }); + + app.MapGet("/GrpcUserOrder", async context => + { + await context.Response.WriteAsync( + "Communication with this gRPC endpoint must be made through a gRPC client."); + }); + } +} \ No newline at end of file diff --git a/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/ProtoModels/OrderGrpcServiceModels.proto b/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/ProtoModels/OrderGrpcServiceModels.proto new file mode 100644 index 0000000..4aba0a2 --- /dev/null +++ b/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/ProtoModels/OrderGrpcServiceModels.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option csharp_namespace = "CleanArc.Web.Plugins.Grpc.ProtoModels"; +import "google/protobuf/empty.proto"; + +package GrpcOrderController; + + +service OrderServices { + rpc GetUserOrders(google.protobuf.Empty) returns (stream GetUserOrdersModel); +} + +message GetUserOrdersModel{ + int32 OrderId=1; + string OrderName=2; +} + diff --git a/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/ProtoModels/UserGrpcServiceModels.proto b/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/ProtoModels/UserGrpcServiceModels.proto new file mode 100644 index 0000000..5c174e0 --- /dev/null +++ b/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/ProtoModels/UserGrpcServiceModels.proto @@ -0,0 +1,44 @@ + +syntax = "proto3"; + +option csharp_namespace = "CleanArc.Web.Plugins.Grpc.ProtoModels"; + +package GrpcUserController; + +service UserServices { + rpc TokenRequest(UserTokenRequest) returns (TokenRequestResult); + rpc GetUserToken(GetUserTokenRequestModel) returns (GetUserTokenRequestResult); +} + +message UserTokenRequest{ + string PhoneNumber=1; +} + +message TokenRequestResult{ + string Message=1; + bool IsSuccess=2; + TokenRequestResultModel UserTokenRequestResult=3; +} + +message TokenRequestResultModel{ + string UserKey=1; +} + +message GetUserTokenRequestModel +{ + string UserKey=1; + string Code=2; +} + +message GetUserTokenRequestResult{ + string Message=1; + bool IsSuccess=2; + UserToken Token=3; +} + +message UserToken{ + string AccessToken=1; + string RefreshToken=2; + string TokenType=3; + int32 ExpiresIn=4; +} \ No newline at end of file diff --git a/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/Services/OrderGrpcServices.cs b/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/Services/OrderGrpcServices.cs new file mode 100644 index 0000000..e1e4597 --- /dev/null +++ b/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/Services/OrderGrpcServices.cs @@ -0,0 +1,46 @@ +using CleanArc.Application.Features.Order.Queries.GetUserOrders; +using CleanArc.SharedKernel.Extensions; +using CleanArc.Web.Plugins.Grpc.ProtoModels; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using Mediator; +using Microsoft.AspNetCore.Authorization; + +namespace CleanArc.Web.Plugins.Grpc.Services +{ + [Authorize] + public class OrderGrpcServices:OrderServices.OrderServicesBase + { + + + private readonly ISender _sender; + + public OrderGrpcServices(ISender sender) + { + _sender = sender; + } + + public override async Task GetUserOrders(Empty request, IServerStreamWriter responseStream, ServerCallContext context) + { + var userId = int.Parse(context.GetHttpContext().User.Identity.GetUserId()); + + var query = await _sender.Send(new GetUserOrdersQueryModel(userId)); + + if (!query.IsSuccess) + { + context.Status = new Status(StatusCode.InvalidArgument, query.GetErrorMessage()); + return; + } + + foreach (var getUsersQueryResultModel in query.Result) + { + await responseStream.WriteAsync(new GetUserOrdersModel() + { OrderId = getUsersQueryResultModel.OrderId, OrderName = getUsersQueryResultModel.OrderName }); + + await Task.Delay(400); + + } + + } + } +} diff --git a/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/Services/UserGrpcServices.cs b/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/Services/UserGrpcServices.cs new file mode 100644 index 0000000..b0223ce --- /dev/null +++ b/server/src/API/Plugins/CleanArc.Web.Plugins.Grpc/Services/UserGrpcServices.cs @@ -0,0 +1,83 @@ +using CleanArc.Application.Features.Users.Queries.GenerateUserToken; +using CleanArc.Application.Features.Users.Queries.TokenRequest; +using CleanArc.Web.Plugins.Grpc.ProtoModels; +using Grpc.Core; +using Mediator; + +namespace CleanArc.Web.Plugins.Grpc.Services; + +public class UserGrpcServices : UserServices.UserServicesBase +{ + private readonly IMediator _mediator; + + public UserGrpcServices(IMediator mediator) + { + _mediator = mediator; + } + + public override async Task TokenRequest(UserTokenRequest request, ServerCallContext context) + { + if (request is null) + { + context.Status = new Status(StatusCode.InvalidArgument, "Required Arguments Not Found"); + + return new TokenRequestResult() { IsSuccess = false, Message = "Input Model Not Found" }; + } + + var tokenQuery = await _mediator.Send(new UserTokenRequestQuery(request.PhoneNumber)); + + if (!tokenQuery.IsSuccess) + { + context.Status = new Status(StatusCode.InvalidArgument, "User not found"); + + return new TokenRequestResult() + { IsSuccess = false, Message = tokenQuery.GetErrorMessage(), UserTokenRequestResult = null }; + } + + if (tokenQuery.IsNotFound) + { + context.Status = new Status(StatusCode.NotFound, "User Not Found"); + + return new TokenRequestResult() + { IsSuccess = false, Message = tokenQuery.GetErrorMessage(), UserTokenRequestResult = null }; + } + + return new TokenRequestResult() + { + IsSuccess = true, + Message = string.Empty, + UserTokenRequestResult = new TokenRequestResultModel() { UserKey = tokenQuery.Result.UserKey } + }; + } + + public override async Task GetUserToken(GetUserTokenRequestModel request, ServerCallContext context) + { + + if (request is null) + { + context.Status = new Status(StatusCode.InvalidArgument, "Required Arguments Not Found"); + + return new GetUserTokenRequestResult() { IsSuccess = false, Message = "Input Model Not Found" }; + } + + var tokenQuery = await _mediator.Send(new GenerateUserTokenQuery(request.UserKey, request.Code)); + + if (!tokenQuery.IsSuccess) + { + context.Status = new Status(StatusCode.InvalidArgument, tokenQuery.GetErrorMessage()); + + return new GetUserTokenRequestResult() { IsSuccess = false, Message = tokenQuery.GetErrorMessage() }; + } + + + return new GetUserTokenRequestResult() + { + IsSuccess = true, Message = string.Empty, + Token = new UserToken() + { + AccessToken = tokenQuery.Result.access_token, ExpiresIn = tokenQuery.Result.expires_in, + RefreshToken = tokenQuery.Result.refresh_token, TokenType = tokenQuery.Result.token_type + } + }; + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/CleanArc.Application.csproj b/server/src/Core/CleanArc.Application/CleanArc.Application.csproj new file mode 100644 index 0000000..fac9978 --- /dev/null +++ b/server/src/Core/CleanArc.Application/CleanArc.Application.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + diff --git a/server/src/Core/CleanArc.Application/Common/LoggingBehavior.cs b/server/src/Core/CleanArc.Application/Common/LoggingBehavior.cs new file mode 100644 index 0000000..a4512f5 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Common/LoggingBehavior.cs @@ -0,0 +1,34 @@ +using CleanArc.Application.Models.Common; +using Mediator; +using Microsoft.Extensions.Logging; + +namespace CleanArc.Application.Common; + +public class LoggingBehavior(ILogger> logger) + : IPipelineBehavior + where TResponse : class + where TRequest : IRequest +{ + + public async ValueTask Handle(TRequest message, MessageHandlerDelegate next, CancellationToken cancellationToken) + { + try + { + var response = await next(message,cancellationToken); + return response; + } + catch (Exception e) + { + logger.LogError(e, e.Message); + + if (typeof(TResponse).GetGenericTypeDefinition() == typeof(OperationResult<>)) + { + var response = new OperationResult { IsException = true }; + + return response as TResponse; + } + + return default; + } + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Common/MetricsBehaviour.cs b/server/src/Core/CleanArc.Application/Common/MetricsBehaviour.cs new file mode 100644 index 0000000..2ead0d6 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Common/MetricsBehaviour.cs @@ -0,0 +1,33 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; +using Mediator; + +namespace CleanArc.Application.Common; + +public class MetricsBehaviour : + IPipelineBehavior where TRequest : IRequest +{ + private readonly Histogram _requestResponseDurationHistogram; + + public MetricsBehaviour(IMeterFactory meterFactory) + { + var meter = meterFactory.Create("mediator_meter"); + _requestResponseDurationHistogram = meter.CreateHistogram( + "Request_Response_Duration", "ms" + , "Determines the total request response durations"); + } + + + public async ValueTask Handle(TRequest message, MessageHandlerDelegate next, CancellationToken cancellationToken) + { + var stopWatch = Stopwatch.StartNew(); + + var response = await next(message,cancellationToken); + + stopWatch.Stop(); + + _requestResponseDurationHistogram.Record(stopWatch.ElapsedMilliseconds,new []{new KeyValuePair("Request",message.GetType().Name)}); + + return response; + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Common/ValidateCommandBehavior.cs b/server/src/Core/CleanArc.Application/Common/ValidateCommandBehavior.cs new file mode 100644 index 0000000..5ae1d47 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Common/ValidateCommandBehavior.cs @@ -0,0 +1,40 @@ +using CleanArc.Application.Models.Common; +using FluentValidation; +using FluentValidation.Results; +using Mediator; + +namespace CleanArc.Application.Common; + +public class ValidateCommandBehavior(IEnumerable> validators) + : IPipelineBehavior + where TResponse : IOperationResult, new() + where TRequest : IRequest +{ + + public async ValueTask Handle(TRequest message, MessageHandlerDelegate next, CancellationToken cancellationToken) + { + var errors = new List(); + + + foreach (var validator in validators) + { + var validationResult = + await validator.ValidateAsync(new ValidationContext(message), cancellationToken); + + if (!validationResult.IsValid) + errors.AddRange(validationResult.Errors); + } + + if (errors.Any()) + { + return new TResponse() + { + ErrorMessages = errors.Select(c => new KeyValuePair(c.PropertyName, c.ErrorMessage)) + .ToList() + }; + } + + + return await next(message, cancellationToken); + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Contracts/IJwtService.cs b/server/src/Core/CleanArc.Application/Contracts/IJwtService.cs new file mode 100644 index 0000000..24ef87e --- /dev/null +++ b/server/src/Core/CleanArc.Application/Contracts/IJwtService.cs @@ -0,0 +1,13 @@ +using System.Security.Claims; +using CleanArc.Application.Models.Jwt; +using CleanArc.Domain.Entities.User; + +namespace CleanArc.Application.Contracts; + +public interface IJwtService +{ + Task GenerateAsync(User user); + Task GetPrincipalFromExpiredToken(string token); + Task GenerateByPhoneNumberAsync(string phoneNumber); + Task RefreshToken(Guid refreshTokenId); +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Contracts/Identity/IAppUserManager.cs b/server/src/Core/CleanArc.Application/Contracts/Identity/IAppUserManager.cs new file mode 100644 index 0000000..4d1e768 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Contracts/Identity/IAppUserManager.cs @@ -0,0 +1,31 @@ +using CleanArc.Domain.Entities.User; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Application.Contracts.Identity; + +public interface IAppUserManager +{ + Task CreateUser(User user); + Task CreateUser(User user,string password); + Task IsExistUser(string phoneNumber); + Task IsExistUserName(string userName); + Task GeneratePhoneNumberConfirmationToken(User user, string phoneNumber); + Task GetUserByCode(string code); + Task ChangePhoneNumber(User user, string phoneNumber, string code); + Task VerifyUserCode(User user,string code); + Task GenerateOtpCode(User user); + Task GetUserByPhoneNumber(string phoneNumber); + Task GetByUserName(string userName); + Task GetUserByIdAsync(int userId); + Task> GetAllUsersAsync(); + Task CreateUserWithPasswordAsync(User user,string password); + Task AddUserToRoleAsync(User user, Role role); + Task IncrementAccessFailedCountAsync(User user); + Task IsUserLockedOutAsync(User user); + Task ResetUserLockoutAsync(User user); + Task UpdateUserAsync(User user); + Task UpdateSecurityStampAsync(User user); + + Task IsPasswordValidAsync(User user, string password); + Task GetRoleAsync(User user); +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Contracts/Identity/IRoleManagerService.cs b/server/src/Core/CleanArc.Application/Contracts/Identity/IRoleManagerService.cs new file mode 100644 index 0000000..a1f181b --- /dev/null +++ b/server/src/Core/CleanArc.Application/Contracts/Identity/IRoleManagerService.cs @@ -0,0 +1,16 @@ +using CleanArc.Application.Models.Identity; +using CleanArc.Domain.Entities.User; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Application.Contracts.Identity; + +public interface IRoleManagerService +{ + Task> GetRolesAsync(); + Task CreateRoleAsync(CreateRoleDto model); + Task DeleteRoleAsync(int roleId); + Task> GetPermissionActionsAsync(); + Task GetRolePermissionsAsync(int roleId); + Task ChangeRolePermissionsAsync(EditRolePermissionsDto model); + Task GetRoleByIdAsync(int roleId); +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Contracts/Persistence/IOrderRepository.cs b/server/src/Core/CleanArc.Application/Contracts/Persistence/IOrderRepository.cs new file mode 100644 index 0000000..aa0c061 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Contracts/Persistence/IOrderRepository.cs @@ -0,0 +1,12 @@ +using CleanArc.Domain.Entities.Order; + +namespace CleanArc.Application.Contracts.Persistence; + +public interface IOrderRepository +{ + Task AddOrderAsync(Order order); + Task> GetAllUserOrdersAsync(int userId); + Task> GetAllOrdersWithRelatedUserAsync(); + Task GetUserOrderByIdAndUserIdAsync(int userId,int orderId,bool trackEntity); + Task DeleteUserOrdersAsync(int userId); +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Contracts/Persistence/IUnitOfWork.cs b/server/src/Core/CleanArc.Application/Contracts/Persistence/IUnitOfWork.cs new file mode 100644 index 0000000..972072f --- /dev/null +++ b/server/src/Core/CleanArc.Application/Contracts/Persistence/IUnitOfWork.cs @@ -0,0 +1,9 @@ +namespace CleanArc.Application.Contracts.Persistence; + +public interface IUnitOfWork +{ + public IUserRefreshTokenRepository UserRefreshTokenRepository { get; } + public IOrderRepository OrderRepository { get; } + Task CommitAsync(); + ValueTask RollBackAsync(); +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Contracts/Persistence/IUserRefreshTokenRepository.cs b/server/src/Core/CleanArc.Application/Contracts/Persistence/IUserRefreshTokenRepository.cs new file mode 100644 index 0000000..be0be93 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Contracts/Persistence/IUserRefreshTokenRepository.cs @@ -0,0 +1,11 @@ +using CleanArc.Domain.Entities.User; + +namespace CleanArc.Application.Contracts.Persistence; + +public interface IUserRefreshTokenRepository +{ + Task CreateToken(int userId); + Task GetTokenWithInvalidation(Guid id); + Task GetUserByRefreshToken(Guid tokenId); + Task RemoveUserOldTokens(int userId, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Admin/Commands/AddAdminCommand/AddAdminCommand.Handler.cs b/server/src/Core/CleanArc.Application/Features/Admin/Commands/AddAdminCommand/AddAdminCommand.Handler.cs new file mode 100644 index 0000000..4680947 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Admin/Commands/AddAdminCommand/AddAdminCommand.Handler.cs @@ -0,0 +1,44 @@ +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Models.Common; +using CleanArc.Domain.Entities.User; +using CleanArc.SharedKernel.Extensions; +using Mediator; + +namespace CleanArc.Application.Features.Admin.Commands.AddAdminCommand +{ + internal class AddAdminCommandHandler:IRequestHandler> + { + private readonly IAppUserManager _userManager; + private readonly IRoleManagerService _roleManagerService; + + public AddAdminCommandHandler(IAppUserManager userManager, IRoleManagerService roleManagerService) + { + _userManager = userManager; + _roleManagerService = roleManagerService; + } + + public async ValueTask> Handle(AddAdminCommand request, CancellationToken cancellationToken) + { + var role = await _roleManagerService.GetRoleByIdAsync(request.RoleId); + + if(role is null) + return OperationResult.NotFoundResult("Specified role not found"); + + var newAdmin = new User { UserName = request.UserName, Email = request.Email }; + + var adminCreateResult = + await _userManager.CreateUserWithPasswordAsync( + newAdmin, request.Password); + + if(!adminCreateResult.Succeeded) + return OperationResult.FailureResult(adminCreateResult.Errors.StringifyIdentityResultErrors()); + + var addAdminToRoleResult = await _userManager.AddUserToRoleAsync(newAdmin, role); + + if(addAdminToRoleResult.Succeeded) + return OperationResult.SuccessResult(true); + + return OperationResult.FailureResult(addAdminToRoleResult.Errors.StringifyIdentityResultErrors()); + } + } +} diff --git a/server/src/Core/CleanArc.Application/Features/Admin/Commands/AddAdminCommand/AddAdminCommand.cs b/server/src/Core/CleanArc.Application/Features/Admin/Commands/AddAdminCommand/AddAdminCommand.cs new file mode 100644 index 0000000..c842030 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Admin/Commands/AddAdminCommand/AddAdminCommand.cs @@ -0,0 +1,31 @@ +using CleanArc.Application.Models.Common; +using CleanArc.SharedKernel.ValidationBase; +using CleanArc.SharedKernel.ValidationBase.Contracts; +using FluentValidation; +using Mediator; + +namespace CleanArc.Application.Features.Admin.Commands.AddAdminCommand; + +public record AddAdminCommand + (string UserName, string Email, string Password, int RoleId) : IRequest>, + IValidatableModel +{ + public IValidator ValidateApplicationModel(ApplicationBaseValidationModelProvider validator) + { + validator.RuleFor(c => c.Email) + .EmailAddress() + .WithMessage("Please enter an valid email"); + + validator.RuleFor(c => c.UserName) + .NotEmpty() + .NotNull() + .WithMessage("Please specify a valid username"); + + validator + .RuleFor(c => c.RoleId) + .GreaterThan(0) + .WithMessage("Please select a valid role"); + + return validator; + } +}; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Admin/Queries/GetToken/AdminGetTokenQuery.Handler.cs b/server/src/Core/CleanArc.Application/Features/Admin/Queries/GetToken/AdminGetTokenQuery.Handler.cs new file mode 100644 index 0000000..773a5e9 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Admin/Queries/GetToken/AdminGetTokenQuery.Handler.cs @@ -0,0 +1,47 @@ +using CleanArc.Application.Contracts; +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Models.Common; +using CleanArc.Application.Models.Jwt; +using Mediator; + +namespace CleanArc.Application.Features.Admin.Queries.GetToken; + +public class AdminGetTokenQueryHandler:IRequestHandler> +{ + private readonly IAppUserManager _userManager; + private readonly IJwtService _jwtService; + public AdminGetTokenQueryHandler(IAppUserManager userManager, IJwtService jwtService) + { + _userManager = userManager; + _jwtService = jwtService; + } + + public async ValueTask> Handle(AdminGetTokenQuery request, CancellationToken cancellationToken) + { + var user = await _userManager.GetByUserName(request.UserName); + + if(user is null) + return OperationResult.FailureResult("User not found"); + + var isUserLockedOut = await _userManager.IsUserLockedOutAsync(user); + + if(isUserLockedOut) + if (user.LockoutEnd != null) + return OperationResult.FailureResult( + $"User is locked out. Try in {(user.LockoutEnd-DateTimeOffset.Now).Value.Minutes} Minutes"); + + var userRoles = await _userManager.GetRoleAsync(user); + + + if(!userRoles.Any()) + return OperationResult.FailureResult("This user does not have any role assigned"); + + if(!await _userManager.IsPasswordValidAsync(user, request.Password)) + return OperationResult.NotFoundResult("User not found"); + + var token= await _jwtService.GenerateAsync(user); + + + return OperationResult.SuccessResult(new(token,userRoles)); + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Admin/Queries/GetToken/AdminGetTokenQuery.Result.cs b/server/src/Core/CleanArc.Application/Features/Admin/Queries/GetToken/AdminGetTokenQuery.Result.cs new file mode 100644 index 0000000..ea68f49 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Admin/Queries/GetToken/AdminGetTokenQuery.Result.cs @@ -0,0 +1,5 @@ +using CleanArc.Application.Models.Jwt; + +namespace CleanArc.Application.Features.Admin.Queries.GetToken; + +public record AdminGetTokenQueryResult(AccessToken Token,string[] Roles); \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Admin/Queries/GetToken/AdminGetTokenQuery.cs b/server/src/Core/CleanArc.Application/Features/Admin/Queries/GetToken/AdminGetTokenQuery.cs new file mode 100644 index 0000000..6c560a6 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Admin/Queries/GetToken/AdminGetTokenQuery.cs @@ -0,0 +1,27 @@ +using CleanArc.Application.Models.Common; +using CleanArc.Application.Models.Jwt; +using CleanArc.SharedKernel.ValidationBase; +using CleanArc.SharedKernel.ValidationBase.Contracts; +using FluentValidation; +using Mediator; + +namespace CleanArc.Application.Features.Admin.Queries.GetToken; + +public record AdminGetTokenQuery(string UserName, string Password) : IRequest>, + IValidatableModel +{ + public IValidator ValidateApplicationModel(ApplicationBaseValidationModelProvider validator) + { + validator.RuleFor(c => c.UserName) + .NotEmpty() + .NotNull() + .WithMessage("Please enter admin username"); + + validator.RuleFor(c => c.Password) + .NotEmpty() + .NotNull() + .WithMessage("Please enter admin password"); + + return validator; + } +}; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Order/Commands/AddOrderCommand.Handler.cs b/server/src/Core/CleanArc.Application/Features/Order/Commands/AddOrderCommand.Handler.cs new file mode 100644 index 0000000..fd886d5 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Order/Commands/AddOrderCommand.Handler.cs @@ -0,0 +1,25 @@ +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Contracts.Persistence; +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Order.Commands; + +internal class AddOrderCommandHandler(IUnitOfWork unitOfWork, IAppUserManager userManager) + : IRequestHandler> +{ + public async ValueTask> Handle(AddOrderCommand request, CancellationToken cancellationToken) + { + var user = await userManager.GetUserByIdAsync(request.UserId); + + if(user==null) + return OperationResult.FailureResult("User Not Found"); + + await unitOfWork.OrderRepository.AddOrderAsync(new Domain.Entities.Order.Order() + { UserId = user.Id, OrderName = request.OrderName }); + + await unitOfWork.CommitAsync(); + + return OperationResult.SuccessResult(true); + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Order/Commands/AddOrderCommand.cs b/server/src/Core/CleanArc.Application/Features/Order/Commands/AddOrderCommand.cs new file mode 100644 index 0000000..5cf8436 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Order/Commands/AddOrderCommand.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; +using CleanArc.Application.Models.Common; +using CleanArc.SharedKernel.ValidationBase; +using CleanArc.SharedKernel.ValidationBase.Contracts; +using FluentValidation; +using Mediator; + +namespace CleanArc.Application.Features.Order.Commands; + +public record AddOrderCommand( string OrderName) : IRequest>, + IValidatableModel +{ + [JsonIgnore] + public int UserId { get; set; } + + public IValidator ValidateApplicationModel(ApplicationBaseValidationModelProvider validator) + { + validator.RuleFor(c => c.OrderName) + .NotEmpty() + .NotNull() + .WithMessage("Please enter your role name"); + + return validator; + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Order/Commands/DeleteUserOrdersCommand.Handler.cs b/server/src/Core/CleanArc.Application/Features/Order/Commands/DeleteUserOrdersCommand.Handler.cs new file mode 100644 index 0000000..86c2466 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Order/Commands/DeleteUserOrdersCommand.Handler.cs @@ -0,0 +1,15 @@ +using CleanArc.Application.Contracts.Persistence; +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Order.Commands; + +public class DeleteUserOrdersCommandHandler(IUnitOfWork unitOfWork) : IRequestHandler> +{ + public async ValueTask> Handle(DeleteUserOrdersCommand request, CancellationToken cancellationToken) + { + await unitOfWork.OrderRepository.DeleteUserOrdersAsync(request.UserId); + + return OperationResult.SuccessResult(true); + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Order/Commands/DeleteUserOrdersCommand.cs b/server/src/Core/CleanArc.Application/Features/Order/Commands/DeleteUserOrdersCommand.cs new file mode 100644 index 0000000..0f95f65 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Order/Commands/DeleteUserOrdersCommand.cs @@ -0,0 +1,6 @@ +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Order.Commands; + +public record DeleteUserOrdersCommand(int UserId):IRequest>; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Order/Commands/UpdateUserOrderCommand.Handler.cs b/server/src/Core/CleanArc.Application/Features/Order/Commands/UpdateUserOrderCommand.Handler.cs new file mode 100644 index 0000000..4a9414c --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Order/Commands/UpdateUserOrderCommand.Handler.cs @@ -0,0 +1,25 @@ +using CleanArc.Application.Contracts.Persistence; +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Order.Commands; + +public class UpdateUserOrderCommandHandler(IUnitOfWork unitOfWork) : IRequestHandler> +{ + + + public async ValueTask> Handle(UpdateUserOrderCommand request, CancellationToken cancellationToken) + { + var order = await unitOfWork.OrderRepository.GetUserOrderByIdAndUserIdAsync(request.UserId, request.OrderId, + true); + + if(order is null) + return OperationResult.NotFoundResult("Specified Order not found"); + + order.OrderName=request.OrderName; + + await unitOfWork.CommitAsync(); + + return OperationResult.SuccessResult(true); + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Order/Commands/UpdateUserOrderCommand.cs b/server/src/Core/CleanArc.Application/Features/Order/Commands/UpdateUserOrderCommand.cs new file mode 100644 index 0000000..9f4f2fe --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Order/Commands/UpdateUserOrderCommand.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; +using CleanArc.Application.Models.Common; +using CleanArc.SharedKernel.ValidationBase; +using CleanArc.SharedKernel.ValidationBase.Contracts; +using FluentValidation; +using Mediator; + +namespace CleanArc.Application.Features.Order.Commands; + +public record UpdateUserOrderCommand + (int OrderId, string OrderName) : IRequest>,IValidatableModel +{ + [JsonIgnore] + public int UserId { get; set; } + + public IValidator ValidateApplicationModel(ApplicationBaseValidationModelProvider validator) + { + validator.RuleFor(c => c.OrderId).NotEmpty().GreaterThan(0); + validator.RuleFor(c => c.OrderName).NotEmpty().NotNull(); + + return validator; + } +}; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Order/Queries/GetAllOrders/GetAllOrdersQuery.cs b/server/src/Core/CleanArc.Application/Features/Order/Queries/GetAllOrders/GetAllOrdersQuery.cs new file mode 100644 index 0000000..418f9a0 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Order/Queries/GetAllOrders/GetAllOrdersQuery.cs @@ -0,0 +1,6 @@ +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Order.Queries.GetAllOrders; + +public record GetAllOrdersQuery():IRequest>>; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Order/Queries/GetAllOrders/GetAllOrdersQueryHandler.cs b/server/src/Core/CleanArc.Application/Features/Order/Queries/GetAllOrders/GetAllOrdersQueryHandler.cs new file mode 100644 index 0000000..66d6593 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Order/Queries/GetAllOrders/GetAllOrdersQueryHandler.cs @@ -0,0 +1,20 @@ +using CleanArc.Application.Contracts.Persistence; +using CleanArc.Application.Models.Common; +using MapsterMapper; +using Mediator; + +namespace CleanArc.Application.Features.Order.Queries.GetAllOrders +{ + internal class GetAllOrdersQueryHandler(IUnitOfWork unitOfWork, IMapper mapper) + : IRequestHandler>> + { + public async ValueTask>> Handle(GetAllOrdersQuery request, CancellationToken cancellationToken) + { + var orders = await unitOfWork.OrderRepository.GetAllOrdersWithRelatedUserAsync(); + + var result = orders.Select(mapper.Map).ToList(); + + return OperationResult>.SuccessResult(result); + } + } +} diff --git a/server/src/Core/CleanArc.Application/Features/Order/Queries/GetAllOrders/GetAllOrdersQueryResult.cs b/server/src/Core/CleanArc.Application/Features/Order/Queries/GetAllOrders/GetAllOrdersQueryResult.cs new file mode 100644 index 0000000..cb3408f --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Order/Queries/GetAllOrders/GetAllOrdersQueryResult.cs @@ -0,0 +1,19 @@ + + +using Mapster; + +namespace CleanArc.Application.Features.Order.Queries.GetAllOrders; + +public record GetAllOrdersQueryResult(int OrderId, string OrderName, int OrderOwnerId, string OrderOwnerUserName); + +class GetAllOrdersQueryResultMapping : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig() + .Map(dest => dest.OrderId, src => src.Id) + .Map(dest => dest.OrderName, src => src.OrderName) + .Map(dest => dest.OrderOwnerId, src => src.User.Id) + .Map(dest => dest.OrderOwnerUserName, src => src.User.UserName); + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Order/Queries/GetUserOrders/GetUserOrdersQueryHandler.cs b/server/src/Core/CleanArc.Application/Features/Order/Queries/GetUserOrders/GetUserOrdersQueryHandler.cs new file mode 100644 index 0000000..c3aeae4 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Order/Queries/GetUserOrders/GetUserOrdersQueryHandler.cs @@ -0,0 +1,21 @@ +using CleanArc.Application.Contracts.Persistence; +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Order.Queries.GetUserOrders; + +internal class GetUserOrdersQueryHandler(IUnitOfWork unitOfWork) + : IRequestHandler>> +{ + public async ValueTask>> Handle(GetUserOrdersQueryModel request, CancellationToken cancellationToken) + { + var orders = await unitOfWork.OrderRepository.GetAllUserOrdersAsync(request.UserId); + + if(!orders.Any()) + return OperationResult>.NotFoundResult("You Don't Have Any Orders"); + + var result = orders.Select(c => new GetUsersQueryResultModel(c.Id, c.OrderName)); + + return OperationResult>.SuccessResult(result.ToList()); + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Order/Queries/GetUserOrders/GetUserOrdersQueryModel.cs b/server/src/Core/CleanArc.Application/Features/Order/Queries/GetUserOrders/GetUserOrdersQueryModel.cs new file mode 100644 index 0000000..8dbf0f3 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Order/Queries/GetUserOrders/GetUserOrdersQueryModel.cs @@ -0,0 +1,6 @@ +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Order.Queries.GetUserOrders; + +public record GetUserOrdersQueryModel(int UserId) : IRequest>>; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Order/Queries/GetUserOrders/GetUsersQueryResultModel.cs b/server/src/Core/CleanArc.Application/Features/Order/Queries/GetUserOrders/GetUsersQueryResultModel.cs new file mode 100644 index 0000000..40467e3 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Order/Queries/GetUserOrders/GetUsersQueryResultModel.cs @@ -0,0 +1,3 @@ +namespace CleanArc.Application.Features.Order.Queries.GetUserOrders; + +public record GetUsersQueryResultModel(int OrderId, string OrderName); \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Role/Commands/AddRoleCommand/AddRoleCommand.Handler.cs b/server/src/Core/CleanArc.Application/Features/Role/Commands/AddRoleCommand/AddRoleCommand.Handler.cs new file mode 100644 index 0000000..85f06ec --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Role/Commands/AddRoleCommand/AddRoleCommand.Handler.cs @@ -0,0 +1,24 @@ +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Models.Common; +using CleanArc.Application.Models.Identity; +using Mediator; + +namespace CleanArc.Application.Features.Role.Commands.AddRoleCommand +{ + internal class AddRoleCommandHandler(IRoleManagerService roleManagerService) + : IRequestHandler> + { + public async ValueTask> Handle(AddRoleCommand request, CancellationToken cancellationToken) + { + var addRoleResult = + await roleManagerService.CreateRoleAsync(new CreateRoleDto() { RoleName = request.RoleName }); + + if (addRoleResult.Succeeded) + return OperationResult.SuccessResult(true); + + var errors = string.Join("\n", addRoleResult.Errors.Select(c => c.Description)); + + return OperationResult.FailureResult(errors); + } + } +} diff --git a/server/src/Core/CleanArc.Application/Features/Role/Commands/AddRoleCommand/AddRoleCommand.cs b/server/src/Core/CleanArc.Application/Features/Role/Commands/AddRoleCommand/AddRoleCommand.cs new file mode 100644 index 0000000..edfff41 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Role/Commands/AddRoleCommand/AddRoleCommand.cs @@ -0,0 +1,22 @@ +using CleanArc.Application.Models.Common; +using CleanArc.SharedKernel.ValidationBase; +using CleanArc.SharedKernel.ValidationBase.Contracts; +using FluentValidation; +using Mediator; + +namespace CleanArc.Application.Features.Role.Commands.AddRoleCommand; + +public record AddRoleCommand(string RoleName) : IRequest>, + IValidatableModel +{ + public IValidator ValidateApplicationModel(ApplicationBaseValidationModelProvider validator) + { + validator + .RuleFor(c => c.RoleName) + .NotEmpty() + .NotNull() + .WithMessage("Please enter role name"); + + return validator; + } +}; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Role/Commands/UpdateRoleClaimsCommand/UpdateRoleClaimsCommand.Handler.cs b/server/src/Core/CleanArc.Application/Features/Role/Commands/UpdateRoleClaimsCommand/UpdateRoleClaimsCommand.Handler.cs new file mode 100644 index 0000000..a7b0d12 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Role/Commands/UpdateRoleClaimsCommand/UpdateRoleClaimsCommand.Handler.cs @@ -0,0 +1,21 @@ +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Models.Common; +using CleanArc.Application.Models.Identity; +using Mediator; + +namespace CleanArc.Application.Features.Role.Commands.UpdateRoleClaimsCommand +{ + internal class UpdateRoleClaimsCommandHandler(IRoleManagerService roleManagerService) + : IRequestHandler> + { + public async ValueTask> Handle(UpdateRoleClaimsCommand request, CancellationToken cancellationToken) + { + var updateRoleResult = await roleManagerService.ChangeRolePermissionsAsync(new EditRolePermissionsDto() + { RoleId = request.RoleId, Permissions = request.RoleClaimValue }); + + return updateRoleResult + ? OperationResult.SuccessResult(true) + : OperationResult.FailureResult("Could Not Update Claims for given Role"); + } + } +} diff --git a/server/src/Core/CleanArc.Application/Features/Role/Commands/UpdateRoleClaimsCommand/UpdateRoleClaimsCommand.cs b/server/src/Core/CleanArc.Application/Features/Role/Commands/UpdateRoleClaimsCommand/UpdateRoleClaimsCommand.cs new file mode 100644 index 0000000..b5c92cc --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Role/Commands/UpdateRoleClaimsCommand/UpdateRoleClaimsCommand.cs @@ -0,0 +1,7 @@ + +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Role.Commands.UpdateRoleClaimsCommand; + +public record UpdateRoleClaimsCommand( int RoleId, List RoleClaimValue):IRequest>; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAllRolesQuery/GetAllRolesQuery.Handler.cs b/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAllRolesQuery/GetAllRolesQuery.Handler.cs new file mode 100644 index 0000000..bd5fb48 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAllRolesQuery/GetAllRolesQuery.Handler.cs @@ -0,0 +1,22 @@ +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Role.Queries.GetAllRolesQuery +{ + internal class GetAllRolesQueryHandler(IRoleManagerService roleManagerService) + : IRequestHandler>> + { + public async ValueTask>> Handle(GetAllRolesQuery request, CancellationToken cancellationToken) + { + var roles = await roleManagerService.GetRolesAsync(); + + if(!roles.Any()) + return OperationResult>.NotFoundResult("No Roles Found"); + + var result = roles.Select(c => new GetAllRolesQueryResponse(int.Parse(c.Id), c.Name)).ToList(); + + return OperationResult>.SuccessResult(result); + } + } +} diff --git a/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAllRolesQuery/GetAllRolesQuery.Response.cs b/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAllRolesQuery/GetAllRolesQuery.Response.cs new file mode 100644 index 0000000..215e83c --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAllRolesQuery/GetAllRolesQuery.Response.cs @@ -0,0 +1,3 @@ +namespace CleanArc.Application.Features.Role.Queries.GetAllRolesQuery; + +public record GetAllRolesQueryResponse(int RoleId,string RoleName); \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAllRolesQuery/GetAllRolesQuery.cs b/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAllRolesQuery/GetAllRolesQuery.cs new file mode 100644 index 0000000..a90b005 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAllRolesQuery/GetAllRolesQuery.cs @@ -0,0 +1,6 @@ +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Role.Queries.GetAllRolesQuery; + +public record GetAllRolesQuery():IRequest>>; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAuthorizableRoutesQuery/GetAuthorizableRoutesQuery.Handler.cs b/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAuthorizableRoutesQuery/GetAuthorizableRoutesQuery.Handler.cs new file mode 100644 index 0000000..6048b77 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAuthorizableRoutesQuery/GetAuthorizableRoutesQuery.Handler.cs @@ -0,0 +1,24 @@ +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Role.Queries.GetAuthorizableRoutesQuery +{ + internal class GetAuthorizableRoutesQueryHandler(IRoleManagerService roleManagerService) + : IRequestHandler>> + { + public async ValueTask>> Handle(GetAuthorizableRoutesQuery request, CancellationToken cancellationToken) + { + var authRoutes = await roleManagerService.GetPermissionActionsAsync(); + + if(!authRoutes.Any()) + return OperationResult>.NotFoundResult("No Special auth route found"); + + var result = authRoutes.Select(c => + new GetAuthorizableRoutesQueryResponse(c.Key, c.AreaName, c.ControllerName, c.ActionName,c.ControllerDescription)) + .ToList(); + + return OperationResult>.SuccessResult(result); + } + } +} diff --git a/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAuthorizableRoutesQuery/GetAuthorizableRoutesQuery.Response.cs b/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAuthorizableRoutesQuery/GetAuthorizableRoutesQuery.Response.cs new file mode 100644 index 0000000..8f7abc9 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAuthorizableRoutesQuery/GetAuthorizableRoutesQuery.Response.cs @@ -0,0 +1,3 @@ +namespace CleanArc.Application.Features.Role.Queries.GetAuthorizableRoutesQuery; + +public record GetAuthorizableRoutesQueryResponse(string RouteKey,string AreaName,string ControllerName,string ActionName,string ControllerDescription); \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAuthorizableRoutesQuery/GetAuthorizableRoutesQuery.cs b/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAuthorizableRoutesQuery/GetAuthorizableRoutesQuery.cs new file mode 100644 index 0000000..d9f9e13 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Role/Queries/GetAuthorizableRoutesQuery/GetAuthorizableRoutesQuery.cs @@ -0,0 +1,6 @@ +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Role.Queries.GetAuthorizableRoutesQuery; + +public record GetAuthorizableRoutesQuery():IRequest>>; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Commands/Create/UserCreateCommand.Handler.cs b/server/src/Core/CleanArc.Application/Features/Users/Commands/Create/UserCreateCommand.Handler.cs new file mode 100644 index 0000000..48d7c82 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Commands/Create/UserCreateCommand.Handler.cs @@ -0,0 +1,54 @@ +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Models.Common; +using CleanArc.Domain.Entities.User; +using MapsterMapper; +using Mediator; +using Microsoft.Extensions.Logging; + +namespace CleanArc.Application.Features.Users.Commands.Create; + +internal class UserCreateCommandHandler( + IAppUserManager userManager, + ILogger logger, + IMapper mapper) + : IRequestHandler> +{ + public async ValueTask> Handle(UserCreateCommand request, + CancellationToken cancellationToken) + { + var userNameExist = await userManager.IsExistUser(request.PhoneNumber); + + if (userNameExist) + return OperationResult.FailureResult("Phone number already exists"); + + var phoneNumberExist = await userManager.IsExistUserName(request.UserName); + + if (phoneNumberExist) + return OperationResult.FailureResult("Username already exists"); + + //var user = new User { UserName = request.UserName, Name = request.FirstName, FamilyName = request.LastName, PhoneNumber = request.PhoneNumber }; + + var user = mapper.Map(request); + + + var createResult =string.IsNullOrEmpty(request.Password)? + await userManager.CreateUser(user) + :await userManager.CreateUser(user, request.Password); + + if (!createResult.Succeeded) + { + return OperationResult.FailureResult(string.Join(",", + createResult.Errors.Select(c => c.Description))); + } + + var code = await userManager.GeneratePhoneNumberConfirmationToken(user, user.PhoneNumber); + + + logger.LogWarning($"Generated Code for User ID {user.Id} is {code}"); + + //TODO Send Code Via Sms Provider + + return OperationResult.SuccessResult(new UserCreateCommandResult + { UserGeneratedKey = user.GeneratedCode }); + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Commands/Create/UserCreateCommand.Result.cs b/server/src/Core/CleanArc.Application/Features/Users/Commands/Create/UserCreateCommand.Result.cs new file mode 100644 index 0000000..4c9b9f5 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Commands/Create/UserCreateCommand.Result.cs @@ -0,0 +1,6 @@ +namespace CleanArc.Application.Features.Users.Commands.Create; + +public class UserCreateCommandResult +{ + public string UserGeneratedKey { get; set; } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Commands/Create/UserCreateCommand.cs b/server/src/Core/CleanArc.Application/Features/Users/Commands/Create/UserCreateCommand.cs new file mode 100644 index 0000000..ccf0a7d --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Commands/Create/UserCreateCommand.cs @@ -0,0 +1,50 @@ +using System.Text.RegularExpressions; +using CleanArc.Application.Models.Common; +using CleanArc.Domain.Entities.User; +using CleanArc.SharedKernel.ValidationBase; +using CleanArc.SharedKernel.ValidationBase.Contracts; +using FluentValidation; +using Mediator; + +namespace CleanArc.Application.Features.Users.Commands.Create; + +public record UserCreateCommand + (string UserName, string Name, string FamilyName, string PhoneNumber,string Password,string RepeatPassword) + : IRequest> + ,IValidatableModel +{ + + public IValidator ValidateApplicationModel(ApplicationBaseValidationModelProvider validator) + { + + validator + .RuleFor(c => c.Name) + .NotEmpty() + .NotNull() + .WithMessage("User must have first name"); + + validator.RuleFor(c => c.UserName) + .NotEmpty() + .NotNull() + .WithMessage("Please enter your username"); + + validator + .RuleFor(c => c.FamilyName) + .NotEmpty() + .NotNull() + .WithMessage("User must have last name"); + + + validator.RuleFor(c => c.PhoneNumber).NotEmpty() + .NotNull().WithMessage("Phone Number is required.") + .MinimumLength(10).WithMessage("PhoneNumber must not be less than 10 characters.") + .MaximumLength(20).WithMessage("PhoneNumber must not exceed 50 characters.") + .Matches(new Regex(@"^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$")).WithMessage("Phone number is not valid"); + + validator.RuleFor(c => c.Password) + .Matches(c => c.RepeatPassword) + .WithMessage("passwords do not match"); + + return validator; + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Commands/RefreshUserTokenCommand/RefreshUserTokenCommand.Handler.cs b/server/src/Core/CleanArc.Application/Features/Users/Commands/RefreshUserTokenCommand/RefreshUserTokenCommand.Handler.cs new file mode 100644 index 0000000..a5a8faf --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Commands/RefreshUserTokenCommand/RefreshUserTokenCommand.Handler.cs @@ -0,0 +1,21 @@ +using CleanArc.Application.Contracts; +using CleanArc.Application.Models.Common; +using CleanArc.Application.Models.Jwt; +using Mediator; + +namespace CleanArc.Application.Features.Users.Commands.RefreshUserTokenCommand +{ + internal class RefreshUserTokenCommandHandler(IJwtService jwtService) + : IRequestHandler> + { + public async ValueTask> Handle(RefreshUserTokenCommand request, CancellationToken cancellationToken) + { + var newToken = await jwtService.RefreshToken(request.RefreshToken); + + if(newToken is null) + return OperationResult.FailureResult("Invalid refresh token"); + + return OperationResult.SuccessResult(newToken); + } + } +} diff --git a/server/src/Core/CleanArc.Application/Features/Users/Commands/RefreshUserTokenCommand/RefreshUserTokenCommand.cs b/server/src/Core/CleanArc.Application/Features/Users/Commands/RefreshUserTokenCommand/RefreshUserTokenCommand.cs new file mode 100644 index 0000000..c5f3e7a --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Commands/RefreshUserTokenCommand/RefreshUserTokenCommand.cs @@ -0,0 +1,22 @@ +using CleanArc.Application.Models.Common; +using CleanArc.Application.Models.Jwt; +using CleanArc.SharedKernel.ValidationBase; +using CleanArc.SharedKernel.ValidationBase.Contracts; +using FluentValidation; +using Mediator; + +namespace CleanArc.Application.Features.Users.Commands.RefreshUserTokenCommand; + +public record RefreshUserTokenCommand(Guid RefreshToken) : IRequest>, + IValidatableModel +{ + public IValidator ValidateApplicationModel(ApplicationBaseValidationModelProvider validator) + { + validator.RuleFor(c => c.RefreshToken) + .NotEmpty() + .NotNull() + .WithMessage("Please enter valid user refresh token"); + + return validator; + } +}; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Commands/RequestLogout/RequestLogoutCommand.Handler.cs b/server/src/Core/CleanArc.Application/Features/Users/Commands/RequestLogout/RequestLogoutCommand.Handler.cs new file mode 100644 index 0000000..dd0192c --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Commands/RequestLogout/RequestLogoutCommand.Handler.cs @@ -0,0 +1,22 @@ +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Users.Commands.RequestLogout +{ + internal class RequestLogoutCommandHandler(IAppUserManager userManager) + : IRequestHandler> + { + public async ValueTask> Handle(RequestLogoutCommand request, CancellationToken cancellationToken) + { + var user = await userManager.GetUserByIdAsync(request.UserId); + + if (user == null) + return OperationResult.FailureResult("User not found"); + + await userManager.UpdateSecurityStampAsync(user); + + return OperationResult.SuccessResult(true); + } + } +} diff --git a/server/src/Core/CleanArc.Application/Features/Users/Commands/RequestLogout/RequestLogoutCommand.cs b/server/src/Core/CleanArc.Application/Features/Users/Commands/RequestLogout/RequestLogoutCommand.cs new file mode 100644 index 0000000..e5e1c20 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Commands/RequestLogout/RequestLogoutCommand.cs @@ -0,0 +1,6 @@ +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Users.Commands.RequestLogout; + +public record RequestLogoutCommand(int UserId):IRequest>; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Queries/GenerateUserToken/GenerateUserTokenQuery.Handler.cs b/server/src/Core/CleanArc.Application/Features/Users/Queries/GenerateUserToken/GenerateUserTokenQuery.Handler.cs new file mode 100644 index 0000000..f2236f5 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Queries/GenerateUserToken/GenerateUserTokenQuery.Handler.cs @@ -0,0 +1,33 @@ +using CleanArc.Application.Contracts; +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Models.Common; +using CleanArc.Application.Models.Jwt; +using CleanArc.SharedKernel.Extensions; +using Mediator; + +namespace CleanArc.Application.Features.Users.Queries.GenerateUserToken; + +internal class GenerateUserTokenQueryHandler(IJwtService jwtService, IAppUserManager userManager) + : IRequestHandler> +{ + public async ValueTask> Handle(GenerateUserTokenQuery request, CancellationToken cancellationToken) + { + var user = await userManager.GetUserByCode(request.UserKey); + + if (user is null) + return OperationResult.FailureResult("User Not found"); + + var result = user.PhoneNumberConfirmed? await userManager.VerifyUserCode( + user, request.Code):await userManager.ChangePhoneNumber(user,user.PhoneNumber,request.Code); + + + if (!result.Succeeded) + return OperationResult.FailureResult(result.Errors.StringifyIdentityResultErrors()); + + await userManager.UpdateUserAsync(user); + + var token = await jwtService.GenerateAsync(user); + + return OperationResult.SuccessResult(token); + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Queries/GenerateUserToken/GenerateUserTokenQuery.cs b/server/src/Core/CleanArc.Application/Features/Users/Queries/GenerateUserToken/GenerateUserTokenQuery.cs new file mode 100644 index 0000000..efe1c5e --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Queries/GenerateUserToken/GenerateUserTokenQuery.cs @@ -0,0 +1,28 @@ +using CleanArc.Application.Models.Common; +using CleanArc.Application.Models.Jwt; +using CleanArc.SharedKernel.ValidationBase; +using CleanArc.SharedKernel.ValidationBase.Contracts; +using FluentValidation; +using Mediator; + +namespace CleanArc.Application.Features.Users.Queries.GenerateUserToken; + +public record GenerateUserTokenQuery(string UserKey, string Code) : IRequest>, + IValidatableModel +{ + public IValidator ValidateApplicationModel(ApplicationBaseValidationModelProvider validator) + { + validator.RuleFor(c => c.Code) + .NotEmpty() + .NotNull() + .Length(6) + .WithMessage("User code is not valid"); + + validator.RuleFor(c => c.UserKey) + .NotEmpty() + .NotNull() + .WithMessage("Invalid user key"); + + return validator; + } +}; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Queries/GetUsers/GetUsersQuery.Handler.cs b/server/src/Core/CleanArc.Application/Features/Users/Queries/GetUsers/GetUsersQuery.Handler.cs new file mode 100644 index 0000000..4c9a7ee --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Queries/GetUsers/GetUsersQuery.Handler.cs @@ -0,0 +1,22 @@ +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Models.Common; +using CleanArc.Domain.Entities.User; +using MapsterMapper; +using Mediator; + +namespace CleanArc.Application.Features.Users.Queries.GetUsers; + +internal class GetUsersQueryHandler(IAppUserManager userManager, IMapper mapper) + : IRequestHandler>> +{ + public async ValueTask>> Handle(GetUsersQuery request, CancellationToken cancellationToken) + { + var usersModel = + (await userManager.GetAllUsersAsync()).Select(mapper.Map).ToList(); + + if(!usersModel.Any()) + return OperationResult>.NotFoundResult("No Users Found!"); + + return OperationResult>.SuccessResult(usersModel); + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Queries/GetUsers/GetUsersQuery.Response.cs b/server/src/Core/CleanArc.Application/Features/Users/Queries/GetUsers/GetUsersQuery.Response.cs new file mode 100644 index 0000000..704c8e4 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Queries/GetUsers/GetUsersQuery.Response.cs @@ -0,0 +1,11 @@ + +using CleanArc.Domain.Entities.User; + +namespace CleanArc.Application.Features.Users.Queries.GetUsers; + +public record GetUsersQueryResponse +{ + public string UserName { get; set; } + public string Email { get; set; } + public int UserId { get; set; } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Queries/GetUsers/GetUsersQuery.cs b/server/src/Core/CleanArc.Application/Features/Users/Queries/GetUsers/GetUsersQuery.cs new file mode 100644 index 0000000..c205513 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Queries/GetUsers/GetUsersQuery.cs @@ -0,0 +1,6 @@ +using CleanArc.Application.Models.Common; +using Mediator; + +namespace CleanArc.Application.Features.Users.Queries.GetUsers; + +public record GetUsersQuery : IRequest>>; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/PasswordUserTokenRequestQuery.Handler.cs b/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/PasswordUserTokenRequestQuery.Handler.cs new file mode 100644 index 0000000..1064c5a --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/PasswordUserTokenRequestQuery.Handler.cs @@ -0,0 +1,27 @@ +using CleanArc.Application.Contracts; +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Models.Common; +using CleanArc.Application.Models.Jwt; +using Mediator; + +namespace CleanArc.Application.Features.Users.Queries.TokenRequest; + +public class PasswordUserTokenRequestQueryResult +(IAppUserManager userManager,IJwtService jwtService) +:IRequestHandler> +{ + public async ValueTask> Handle(PasswordUserTokenRequestQuery request, CancellationToken cancellationToken) + { + var user = await userManager.GetByUserName(request.UserName); + + if(user is null) + return OperationResult.NotFoundResult("User not found"); + + if(!await userManager.IsPasswordValidAsync(user,request.Password)) + return OperationResult.NotFoundResult("User not found"); + + var token = await jwtService.GenerateAsync(user); + + return OperationResult.SuccessResult(token); + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/PasswordUserTokenRequestQuery.cs b/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/PasswordUserTokenRequestQuery.cs new file mode 100644 index 0000000..a0d8edd --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/PasswordUserTokenRequestQuery.cs @@ -0,0 +1,24 @@ +using CleanArc.Application.Models.Common; +using CleanArc.Application.Models.Jwt; +using CleanArc.SharedKernel.ValidationBase; +using CleanArc.SharedKernel.ValidationBase.Contracts; +using FluentValidation; +using Mediator; + +namespace CleanArc.Application.Features.Users.Queries.TokenRequest; + +public record PasswordUserTokenRequestQuery +(string UserName,string Password) +:IValidatableModel,IRequest> +{ + public IValidator ValidateApplicationModel(ApplicationBaseValidationModelProvider validator) + { + validator.RuleFor(c => c.UserName) + .NotEmpty(); + + validator.RuleFor(c => c.Password) + .NotEmpty(); + + return validator; + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/UserTokenRequestQuery.Handler.cs b/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/UserTokenRequestQuery.Handler.cs new file mode 100644 index 0000000..0173173 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/UserTokenRequestQuery.Handler.cs @@ -0,0 +1,30 @@ +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Models.Common; +using Mediator; +using Microsoft.Extensions.Logging; + +namespace CleanArc.Application.Features.Users.Queries.TokenRequest; + +public class UserTokenRequestQueryHandler( + IAppUserManager userManager, + ILogger logger) + : IRequestHandler> +{ + + + public async ValueTask> Handle(UserTokenRequestQuery request, CancellationToken cancellationToken) + { + var user = await userManager.GetUserByPhoneNumber(request.UserPhoneNumber); + + if(user is null) + return OperationResult.NotFoundResult("User Not found"); + + var code = user.PhoneNumberConfirmed? await userManager.GenerateOtpCode(user) : await userManager.GeneratePhoneNumberConfirmationToken(user,user.PhoneNumber); + + logger.LogWarning($"Generated Code for user Id {user.Id} is {code}"); + + //TODO Send Code Via Sms Provider + + return OperationResult.SuccessResult(new UserTokenRequestQueryResponse {UserKey = user.GeneratedCode}); + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/UserTokenRequestQuery.Response.cs b/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/UserTokenRequestQuery.Response.cs new file mode 100644 index 0000000..7319039 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/UserTokenRequestQuery.Response.cs @@ -0,0 +1,6 @@ +namespace CleanArc.Application.Features.Users.Queries.TokenRequest; + +public class UserTokenRequestQueryResponse +{ + public string UserKey { get; set; } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/UserTokenRequestQuery.cs b/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/UserTokenRequestQuery.cs new file mode 100644 index 0000000..2b6f92a --- /dev/null +++ b/server/src/Core/CleanArc.Application/Features/Users/Queries/TokenRequest/UserTokenRequestQuery.cs @@ -0,0 +1,25 @@ +using CleanArc.Application.Models.Common; +using CleanArc.SharedKernel.ValidationBase; +using CleanArc.SharedKernel.ValidationBase.Contracts; +using FluentValidation; +using Mediator; +using System.Text.RegularExpressions; + +namespace CleanArc.Application.Features.Users.Queries.TokenRequest; + +public record UserTokenRequestQuery(string UserPhoneNumber) : IRequest>, + IValidatableModel +{ + public IValidator ValidateApplicationModel(ApplicationBaseValidationModelProvider validator) + { + + validator.RuleFor(c => c.UserPhoneNumber).NotEmpty() + .NotNull().WithMessage("Phone Number is required.") + .MinimumLength(10).WithMessage("PhoneNumber must not be less than 10 characters.") + .MaximumLength(20).WithMessage("PhoneNumber must not exceed 50 characters.") + .Matches(new Regex(@"^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$")).WithMessage("Phone number is not valid"); + + + return validator; + } +}; \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Models/ApiResult/ApiResult.cs b/server/src/Core/CleanArc.Application/Models/ApiResult/ApiResult.cs new file mode 100644 index 0000000..1e22939 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Models/ApiResult/ApiResult.cs @@ -0,0 +1,37 @@ +using System.Diagnostics; +using CleanArc.SharedKernel.Extensions; + +namespace CleanArc.Application.Models.ApiResult; + +public class ApiResult +{ + public bool IsSuccess { get; set; } + public ApiResultStatusCode StatusCode { get; set; } + + public string Message { get; set; } + public string RequestId { get; } + + public ApiResult(bool isSuccess, ApiResultStatusCode statusCode, string message = null) + { + IsSuccess = isSuccess; + StatusCode = statusCode; + Message = message ?? statusCode.ToDisplay(); + RequestId = Activity.Current?.TraceId.ToHexString() ?? string.Empty; + } + + +} + +public class ApiResult : ApiResult + +{ + public TData Data { get; set; } + + public ApiResult(bool isSuccess, ApiResultStatusCode statusCode, TData data, string message = null) + : base(isSuccess, statusCode, message) + { + Data = data; + } + + +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Models/ApiResult/ApiResultStatusCodecs.cs b/server/src/Core/CleanArc.Application/Models/ApiResult/ApiResultStatusCodecs.cs new file mode 100644 index 0000000..ca6f63c --- /dev/null +++ b/server/src/Core/CleanArc.Application/Models/ApiResult/ApiResultStatusCodecs.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; + +namespace CleanArc.Application.Models.ApiResult; + +public enum ApiResultStatusCode +{ + [Display(Name = "Success")] + Success = 200, + + [Display(Name = "Server Error")] + ServerError = 500, + + [Display(Name = "Bad Request Error")] + BadRequest = 400, + + [Display(Name = "Not Found")] + NotFound = 404, + + + [Display(Name = "Request Process Error")] + EntityProcessError = 422, + + [Display(Name = "Authentication Error")] + UnAuthorized = 401, + + [Display(Name = "Authorization Error")] + Forbidden = 403, + + [Display(Name = "Not Acceptable")] + NotAcceptable = 406, + + [Display(Name = "Failed Dependency")] + FailedDependency = 424 +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Models/Common/OperationResult.cs b/server/src/Core/CleanArc.Application/Models/Common/OperationResult.cs new file mode 100644 index 0000000..fdc6058 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Models/Common/OperationResult.cs @@ -0,0 +1,62 @@ +namespace CleanArc.Application.Models.Common; + +/// +/// Marker Interface To Mark Operation Result Response In Validation Pipeline +/// +public interface IOperationResult +{ + bool IsSuccess { get; set; } + List> ErrorMessages { get; set; } + bool IsException { get; set; } + bool IsNotFound { get; set; } +} + +public class OperationResult : IOperationResult +{ + public TResult Result { get; set; } + + public bool IsSuccess { get; set; } + public List> ErrorMessages { get; set; } = new(); + public bool IsException { get; set; } + public bool IsNotFound { get; set; } + + public static OperationResult SuccessResult(TResult result) + { + return new OperationResult { Result = result, IsSuccess = true }; + } + + public static OperationResult FailureResult(string propertyName, string message, TResult result = default) + { + var operationResult = new OperationResult { Result = result, IsSuccess = false }; + + operationResult.ErrorMessages.Add(new(propertyName,message)); + + return operationResult; + } + public static OperationResult FailureResult(string message, TResult result = default) + { + var operationResult = new OperationResult { Result = result, IsSuccess = false }; + + operationResult.ErrorMessages.Add(new("GeneralError",message)); + + return operationResult; + } + public static OperationResult NotFoundResult(string message) + { + var operationResult = new OperationResult { IsSuccess = false, IsNotFound = true }; + + operationResult.ErrorMessages.Add(new("GeneralError", message)); + + return operationResult; + } + + public void AddError(string propertyName, string message) + { + IsSuccess = false; + + ErrorMessages.Add(new(propertyName,message)); + } + + public string GetErrorMessage() + => string.Join(Environment.NewLine, ErrorMessages.Select(x => $"{x.Key}: {x.Value}")); +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Models/Identity/ActionDescriptionDto.cs b/server/src/Core/CleanArc.Application/Models/Identity/ActionDescriptionDto.cs new file mode 100644 index 0000000..df87808 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Models/Identity/ActionDescriptionDto.cs @@ -0,0 +1,16 @@ +namespace CleanArc.Application.Models.Identity; + +public class ActionDescriptionDto +{ + public string Key => $"{AreaName}:{ControllerName}:{ActionName}"; + + public string AreaName { get; set; } + + public string ControllerName { get; set; } + public string ControllerDisplayName { get; set; } + + public string ActionName { get; set; } + + public string ActionDisplayName { get; set; } + public string ControllerDescription { get; set; } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Models/Identity/CreateRoleDto.cs b/server/src/Core/CleanArc.Application/Models/Identity/CreateRoleDto.cs new file mode 100644 index 0000000..04150a6 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Models/Identity/CreateRoleDto.cs @@ -0,0 +1,6 @@ +namespace CleanArc.Application.Models.Identity; + +public class CreateRoleDto +{ + public string RoleName { get; set; } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Models/Identity/EditRolePermissionsDto.cs b/server/src/Core/CleanArc.Application/Models/Identity/EditRolePermissionsDto.cs new file mode 100644 index 0000000..f955fe4 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Models/Identity/EditRolePermissionsDto.cs @@ -0,0 +1,7 @@ +namespace CleanArc.Application.Models.Identity; + +public class EditRolePermissionsDto +{ + public int RoleId { get; set; } + public List Permissions { get; set; } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Models/Identity/GetRolesDto.cs b/server/src/Core/CleanArc.Application/Models/Identity/GetRolesDto.cs new file mode 100644 index 0000000..fb1cc9c --- /dev/null +++ b/server/src/Core/CleanArc.Application/Models/Identity/GetRolesDto.cs @@ -0,0 +1,9 @@ +using CleanArc.Domain.Entities.User; + +namespace CleanArc.Application.Models.Identity; + +public class GetRolesDto +{ + public string Id { get; set; } + public string Name { get; set; } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Models/Identity/RolePermissionDto.cs b/server/src/Core/CleanArc.Application/Models/Identity/RolePermissionDto.cs new file mode 100644 index 0000000..b2cc930 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Models/Identity/RolePermissionDto.cs @@ -0,0 +1,14 @@ +using CleanArc.Domain.Entities.User; + +namespace CleanArc.Application.Models.Identity; + +public class RolePermissionDto +{ + public List Keys { get; set; } = new List(); + + public Role Role { get; set; } + + public int RoleId { get; set; } + + public List Actions { get; set; } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Models/Jwt/AccessToken.cs b/server/src/Core/CleanArc.Application/Models/Jwt/AccessToken.cs new file mode 100644 index 0000000..c836d71 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Models/Jwt/AccessToken.cs @@ -0,0 +1,19 @@ +using System.IdentityModel.Tokens.Jwt; + +namespace CleanArc.Application.Models.Jwt; + +public class AccessToken +{ + public string access_token { get; set; } + public string refresh_token { get; set; } + public string token_type { get; set; } + public int expires_in { get; set; } + + public AccessToken(JwtSecurityToken securityToken,string refreshToken="") + { + access_token = new JwtSecurityTokenHandler().WriteToken(securityToken); + token_type = "Bearer"; + expires_in = (int)(securityToken.ValidTo - DateTime.UtcNow).TotalSeconds; + refresh_token = refreshToken; + } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/Models/Jwt/TokenRequest.cs b/server/src/Core/CleanArc.Application/Models/Jwt/TokenRequest.cs new file mode 100644 index 0000000..b4b0773 --- /dev/null +++ b/server/src/Core/CleanArc.Application/Models/Jwt/TokenRequest.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace CleanArc.Application.Models.Jwt; + +public class TokenRequest +{ + [Required] + public string grant_type { get; set; } + public string username { get; set; } + public string password { get; set; } + public string refresh_token { get; set; } + public string scope { get; set; } + + public string client_id { get; set; } + public string client_secret { get; set; } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Application/ServiceConfiguration/ServiceCollectionExtension.cs b/server/src/Core/CleanArc.Application/ServiceConfiguration/ServiceCollectionExtension.cs new file mode 100644 index 0000000..a4610a7 --- /dev/null +++ b/server/src/Core/CleanArc.Application/ServiceConfiguration/ServiceCollectionExtension.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using CleanArc.Application.Common; +using Mapster; +using Mediator; +using Microsoft.Extensions.DependencyInjection; + +namespace CleanArc.Application.ServiceConfiguration; + +public static class ServiceCollectionExtension +{ + public static IServiceCollection AddApplicationServices(this IServiceCollection services) + { + services.AddMediator(options => + { + options.ServiceLifetime = ServiceLifetime.Scoped; + options.Namespace = "CleanArc.Application.Mediator"; + }); + + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(MetricsBehaviour<,>)); + + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidateCommandBehavior<,>)); + + + + return services; + } + + +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Domain/CleanArc.Domain.csproj b/server/src/Core/CleanArc.Domain/CleanArc.Domain.csproj new file mode 100644 index 0000000..d724280 --- /dev/null +++ b/server/src/Core/CleanArc.Domain/CleanArc.Domain.csproj @@ -0,0 +1,11 @@ + + + + net10.0 + enable + + + + + + diff --git a/server/src/Core/CleanArc.Domain/Common/BaseEntity.cs b/server/src/Core/CleanArc.Domain/Common/BaseEntity.cs new file mode 100644 index 0000000..1dea77c --- /dev/null +++ b/server/src/Core/CleanArc.Domain/Common/BaseEntity.cs @@ -0,0 +1,59 @@ +namespace CleanArc.Domain.Common; + +public interface IEntity +{ +} + +public interface ITimeModification +{ + DateTime CreatedTime { get; set; } + DateTime? ModifiedDate { get; set; } +} + +public abstract class BaseEntity : IEntity, ITimeModification +{ + public TKey Id { get; protected set; } + + public override bool Equals(object obj) + { + if (!(obj is BaseEntity other)) + return false; + + if (ReferenceEquals(this, other)) + return true; + + if (GetType() != other.GetType()) + return false; + + return Id.Equals(other.Id); + } + + public static bool operator ==(BaseEntity a, BaseEntity b) + { + if (a is null && b is null) + return true; + + if (a is null || b is null) + return false; + + return a.Equals(b); + } + + public static bool operator !=(BaseEntity a, BaseEntity b) + { + return !(a == b); + } + + public override int GetHashCode() + { + return (GetType().ToString() + Id).GetHashCode(); + } + + public DateTime CreatedTime { get; set; } + public DateTime? ModifiedDate { get; set; } +} + +public abstract class BaseEntity : BaseEntity +{ + +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Domain/Entities/Order/Order.cs b/server/src/Core/CleanArc.Domain/Entities/Order/Order.cs new file mode 100644 index 0000000..77d6dd0 --- /dev/null +++ b/server/src/Core/CleanArc.Domain/Entities/Order/Order.cs @@ -0,0 +1,16 @@ +using CleanArc.Domain.Common; + +namespace CleanArc.Domain.Entities.Order; + +public class Order:BaseEntity +{ + public string OrderName { get; set; } + public bool IsDeleted { get; set; } + + #region Navigation Properties + + public User.User User { get; set; } + public int UserId { get; set; } + + #endregion +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Domain/Entities/User/Role.cs b/server/src/Core/CleanArc.Domain/Entities/User/Role.cs new file mode 100644 index 0000000..fcc4ba7 --- /dev/null +++ b/server/src/Core/CleanArc.Domain/Entities/User/Role.cs @@ -0,0 +1,19 @@ +using CleanArc.Domain.Common; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Domain.Entities.User; + +public class Role:IdentityRole,IEntity +{ + public Role() + { + CreatedDate=DateTime.Now; + } + + public string DisplayName { get; set; } + public DateTime CreatedDate { get; set; } + public ICollection Claims { get; set; } + public ICollection Users { get; set; } + + +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Domain/Entities/User/RoleClaim.cs b/server/src/Core/CleanArc.Domain/Entities/User/RoleClaim.cs new file mode 100644 index 0000000..3c1c5c9 --- /dev/null +++ b/server/src/Core/CleanArc.Domain/Entities/User/RoleClaim.cs @@ -0,0 +1,16 @@ +using CleanArc.Domain.Common; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Domain.Entities.User; + +public class RoleClaim:IdentityRoleClaim,IEntity +{ + public RoleClaim() + { + CreatedClaim=DateTime.Now; + } + + public DateTime CreatedClaim { get; set; } + public Role Role { get; set; } + +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Domain/Entities/User/User.cs b/server/src/Core/CleanArc.Domain/Entities/User/User.cs new file mode 100644 index 0000000..f9a9e61 --- /dev/null +++ b/server/src/Core/CleanArc.Domain/Entities/User/User.cs @@ -0,0 +1,29 @@ +using CleanArc.Domain.Common; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Domain.Entities.User; + +public class User:IdentityUser,IEntity +{ + public User() + { + this.GeneratedCode = Guid.NewGuid().ToString().Substring(0, 8); + } + + public string Name { get; set; } + public string FamilyName { get; set; } + public string GeneratedCode { get; set; } + + public ICollection UserRoles { get; set; } + public ICollection Logins { get; set; } + public ICollection Claims { get; set; } + public ICollection Tokens { get; set; } + public ICollection UserRefreshTokens { get; set; } + + #region Navigation Properties + + public IList Orders { get; set; } + + #endregion + +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Domain/Entities/User/UserClaim.cs b/server/src/Core/CleanArc.Domain/Entities/User/UserClaim.cs new file mode 100644 index 0000000..b6b9abb --- /dev/null +++ b/server/src/Core/CleanArc.Domain/Entities/User/UserClaim.cs @@ -0,0 +1,9 @@ +using CleanArc.Domain.Common; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Domain.Entities.User; + +public class UserClaim:IdentityUserClaim,IEntity +{ + public User User { get; set; } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Domain/Entities/User/UserLogin.cs b/server/src/Core/CleanArc.Domain/Entities/User/UserLogin.cs new file mode 100644 index 0000000..32953f4 --- /dev/null +++ b/server/src/Core/CleanArc.Domain/Entities/User/UserLogin.cs @@ -0,0 +1,15 @@ +using CleanArc.Domain.Common; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Domain.Entities.User; + +public class UserLogin:IdentityUserLogin,IEntity +{ + public UserLogin() + { + LoggedOn=DateTime.Now; + } + + public User User { get; set; } + public DateTime LoggedOn { get; set; } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Domain/Entities/User/UserRefreshToken.cs b/server/src/Core/CleanArc.Domain/Entities/User/UserRefreshToken.cs new file mode 100644 index 0000000..88e2b3b --- /dev/null +++ b/server/src/Core/CleanArc.Domain/Entities/User/UserRefreshToken.cs @@ -0,0 +1,16 @@ +using CleanArc.Domain.Common; + +namespace CleanArc.Domain.Entities.User; + +public class UserRefreshToken:BaseEntity +{ + public UserRefreshToken() + { + CreatedAt=DateTime.Now; + } + + public int UserId { get; set; } + public User User { get; set; } + public DateTime CreatedAt { get; set; } + public bool IsValid { get; set; } +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Domain/Entities/User/UserRole.cs b/server/src/Core/CleanArc.Domain/Entities/User/UserRole.cs new file mode 100644 index 0000000..205df10 --- /dev/null +++ b/server/src/Core/CleanArc.Domain/Entities/User/UserRole.cs @@ -0,0 +1,12 @@ +using CleanArc.Domain.Common; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Domain.Entities.User; + +public class UserRole : IdentityUserRole,IEntity +{ + public User User { get; set; } + public Role Role { get; set; } + public DateTime CreatedUserRoleDate { get; set; } + +} \ No newline at end of file diff --git a/server/src/Core/CleanArc.Domain/Entities/User/UserToken.cs b/server/src/Core/CleanArc.Domain/Entities/User/UserToken.cs new file mode 100644 index 0000000..99f206f --- /dev/null +++ b/server/src/Core/CleanArc.Domain/Entities/User/UserToken.cs @@ -0,0 +1,16 @@ +using CleanArc.Domain.Common; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Domain.Entities.User; + +public class UserToken:IdentityUserToken,IEntity +{ + public UserToken() + { + GeneratedTime=DateTime.Now; + } + + public User User { get; set; } + public DateTime GeneratedTime { get; set; } + +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.CrossCutting/CleanArc.Infrastructure.CrossCutting.csproj b/server/src/Infrastructure/CleanArc.Infrastructure.CrossCutting/CleanArc.Infrastructure.CrossCutting.csproj new file mode 100644 index 0000000..166333e --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.CrossCutting/CleanArc.Infrastructure.CrossCutting.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + + + + + + + + + + + + + + + diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.CrossCutting/Logging/LoggingConfiguration.cs b/server/src/Infrastructure/CleanArc.Infrastructure.CrossCutting/Logging/LoggingConfiguration.cs new file mode 100644 index 0000000..60f38ab --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.CrossCutting/Logging/LoggingConfiguration.cs @@ -0,0 +1,73 @@ +using System.Data; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Enrichers.Span; +using Serilog.Exceptions; +using Serilog.Formatting.Json; +using Serilog.Sinks.MSSqlServer; + +namespace CleanArc.Infrastructure.CrossCutting.Logging; + +public static class LoggingConfiguration +{ + public static Action ConfigureLogger => (context, configuration) => + { + #region Enriching Logger Context + + var env = context.HostingEnvironment; + + + configuration.Enrich.FromLogContext() + .Enrich.WithProperty("ApplicationName", env.ApplicationName) + .Enrich.WithProperty("Environment", env.EnvironmentName) + .Enrich.WithSpan() + .Enrich.WithExceptionDetails(); + + + + #endregion + + + var columnOpts = new ColumnOptions(); + columnOpts.Store.Remove(StandardColumn.Properties); + columnOpts.Store.Add(StandardColumn.LogEvent); + columnOpts.LogEvent.DataLength = 4096; + columnOpts.PrimaryKey = columnOpts.Id; + columnOpts.Id.DataType = SqlDbType.Int; + + if (!context.HostingEnvironment.IsDevelopment()) + { + configuration.WriteTo + .MSSqlServer( + connectionString: context.Configuration.GetConnectionString("logDb"), + sinkOptions: new MSSqlServerSinkOptions { TableName = "LogEvents", AutoCreateSqlTable = true, SchemaName = "log",AutoCreateSqlDatabase = true}) + .MinimumLevel.Warning(); + + } + + else{ + configuration.WriteTo.Console().MinimumLevel.Information(); + configuration.WriteTo.File(new JsonFormatter(), "logs/log.json").MinimumLevel.Information(); + } + + #region ElasticSearch Configuration. UnComment if Needed + + + //var elasticUrl = context.Configuration.GetValue("Logging:ElasticUrl"); + + //if (!string.IsNullOrEmpty(elasticUrl)) + //{ + // configuration.WriteTo.Elasticsearch( + // new ElasticsearchSinkOptions(new Uri(elasticUrl)) + // { + // AutoRegisterTemplate = true, + // AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7, + // IndexFormat = "web-logs-{0:yyyy.MM.dd}", + // MinimumLogEventLevel = LogEventLevel.Debug + // }); + //} + + #endregion + }; +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/CleanArc.Infrastructure.Identity.csproj b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/CleanArc.Infrastructure.Identity.csproj new file mode 100644 index 0000000..93034ab --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/CleanArc.Infrastructure.Identity.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + + + + + + + + + + + + + + + + + diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/AppErrorDescriber.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/AppErrorDescriber.cs new file mode 100644 index 0000000..249a0db --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/AppErrorDescriber.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Infrastructure.Identity.Identity; + +public class AppErrorDescriber:IdentityErrorDescriber +{ + public override IdentityError DefaultError() + { + return new IdentityError + { + Code = "DefaultError", + Description = "There was an error" + }; + } + + public override IdentityError DuplicateEmail(string email) + { + return new IdentityError + { + Code = nameof(DuplicateEmail), + Description = "Specified email already exists" + }; + } + + public override IdentityError DuplicateUserName(string userName) + { + return new IdentityError + { + Code = nameof(DuplicateUserName), + Description = "specified username already exists" + }; + } + + public override IdentityError PasswordMismatch() + { + return new IdentityError + { + Code = nameof(PasswordMismatch), + Description = "Incorrect password" + }; + } + + public override IdentityError PasswordTooShort(int length) + { + return new IdentityError + { + Code = nameof(PasswordTooShort), + Description = "Invalid password. Password is to short" + }; + } + + public override IdentityError InvalidUserName(string userName) + { + return new IdentityError + { + Code = nameof(InvalidUserName), + Description = "Invalid username" + }; + } + + public override IdentityError InvalidEmail(string email) + { + return new IdentityError + { + Code = nameof(InvalidEmail), + Description = "Invalid email" + }; + } + + public override IdentityError InvalidToken() + { + + return new IdentityError + { + Code = nameof(InvalidToken), + Description = "Invalid given code. Please try again" + }; + } + +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/AppUserClaimsPrincipleFactory.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/AppUserClaimsPrincipleFactory.cs new file mode 100644 index 0000000..6ac7472 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/AppUserClaimsPrincipleFactory.cs @@ -0,0 +1,34 @@ +using System.Security.Claims; +using CleanArc.Domain.Entities.User; +using CleanArc.Infrastructure.Identity.Identity.Manager; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; + +namespace CleanArc.Infrastructure.Identity.Identity; + +public class AppUserClaimsPrincipleFactory:UserClaimsPrincipalFactory +{ + public AppUserClaimsPrincipleFactory(AppUserManager userManager, AppRoleManager roleManager, IOptions options) : base(userManager, roleManager, options) + { + } + + + protected override async Task GenerateClaimsAsync(User user) + { + var userRoles = await UserManager.GetRolesAsync(user); + + var claimsIdentity = await base.GenerateClaimsAsync(user); + //claimsIdentity.AddClaim(new Claim(ClaimTypes.Email,user?.Email)); + // claimsIdentity.AddClaim(new Claim(ClaimTypes.MobilePhone,user.PhoneNumber)); + claimsIdentity.AddClaim(new Claim(ClaimTypes.UserData,user.GeneratedCode)); + + foreach (var roles in userRoles) + { + claimsIdentity.AddClaim(new Claim(ClaimTypes.Role,roles)); + } + + //claimsIdentity.AddClaim(new Claim(ClaimTypes.Role,RoleManager.GetRoleNameAsync(user.Roles))); + + return claimsIdentity; + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/KeyRing.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/KeyRing.cs new file mode 100644 index 0000000..e6c0f72 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/KeyRing.cs @@ -0,0 +1,78 @@ +using System.Security.Cryptography; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Infrastructure.Identity.Identity.DataProtection; + +public class KeyRing : ILookupProtectorKeyRing +{ + private readonly IDictionary _keyDictionary = new Dictionary(); + + public KeyRing(IWebHostEnvironment hostingEnvironment) + { + // Create the keyring directory if one doesn't exist. + var keyRingDirectory = Path.Combine(hostingEnvironment.ContentRootPath, "keyring"); + Directory.CreateDirectory(keyRingDirectory); + + var directoryInfo = new DirectoryInfo(keyRingDirectory); + if (directoryInfo.GetFiles("*.key").Length == 0) + { + ProtectorAlgorithmHelper.GetAlgorithms( + ProtectorAlgorithmHelper.DefaultAlgorithm, + out SymmetricAlgorithm encryptionAlgorithm, + out KeyedHashAlgorithm signingAlgorithm, + out int derivationCount); + encryptionAlgorithm.GenerateKey(); + + var keyAsString = Convert.ToBase64String(encryptionAlgorithm.Key); + var keyId = Guid.NewGuid().ToString(); + var keyFileName = Path.Combine(keyRingDirectory, keyId+".key"); + using (var file = File.CreateText(keyFileName)) + { + file.WriteLine(keyAsString); + } + + _keyDictionary.Add(keyId, keyAsString); + + CurrentKeyId = keyId; + + encryptionAlgorithm.Clear(); + encryptionAlgorithm.Dispose(); + signingAlgorithm.Dispose(); + } + else + { + var filesOrdered = directoryInfo.EnumerateFiles() + .OrderByDescending(d => d.CreationTime) + .Select(d => d.Name) + .ToList(); + + foreach (var fileName in filesOrdered) + { + var keyFileName = Path.Combine(keyRingDirectory, fileName); + var key = File.ReadAllText(keyFileName); + var keyId = Path.GetFileNameWithoutExtension(fileName); + _keyDictionary.Add(keyId, key); + CurrentKeyId = keyId; + } + } + } + + public string this[string keyId] + { + get + { + return _keyDictionary[keyId]; + } + } + + public string CurrentKeyId + { + get; private set; + } + + public IEnumerable GetAllKeyIds() + { + return _keyDictionary.Keys; + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/LookupProtector.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/LookupProtector.cs new file mode 100644 index 0000000..bee1c6e --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/LookupProtector.cs @@ -0,0 +1,236 @@ +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Infrastructure.Identity.Identity.DataProtection; + +public class LookupProtector : ILookupProtector +{ + private readonly ProtectorAlgorithm _defaultAlgorithm = ProtectorAlgorithm.Aes256Hmac512; + private readonly ILookupProtectorKeyRing _keyRing; + + public LookupProtector(ILookupProtectorKeyRing keyRing) + { + _keyRing = keyRing; + } + + public string Protect(string keyId, string data) + { + // Get the default algorithms. + // We does this so we can embed the algorithm details used in the cipher text so we can + // change algorithms as yet another collision appears in a hashing algorithm. + // See https://media.blackhat.com/bh-us-10/whitepapers/Sullivan/BlackHat-USA-2010-Sullivan-Cryptographic-Agility-wp.pdf + ProtectorAlgorithmHelper.GetAlgorithms( + _defaultAlgorithm, + out SymmetricAlgorithm encryptingAlgorithm, + out KeyedHashAlgorithm signingAlgorithm, + out int keyDerivationIterationCount); + + var masterKey = MasterKey(keyId); + + // Convert the string to bytes, because encryption works on bytes, not strings. + var plainText = Encoding.UTF8.GetBytes(data); + byte[] cipherTextAndIV; + + // Derive a key for encryption from the master key + encryptingAlgorithm.Key = DerivedEncryptionKey( + masterKey, + encryptingAlgorithm, + keyDerivationIterationCount); + + // As we need this to be deterministic, we need to force an IV that is derived from the plain text. + encryptingAlgorithm.IV = DerivedInitializationVector( + masterKey, + data, + encryptingAlgorithm, + keyDerivationIterationCount); + + // And encrypt + using (var ms = new MemoryStream()) + using (var cs = new CryptoStream( + ms, + encryptingAlgorithm.CreateEncryptor(), + CryptoStreamMode.Write)) + { + cs.Write(plainText); + cs.FlushFinalBlock(); + var encryptedData = ms.ToArray(); + + cipherTextAndIV = CombineByteArrays(encryptingAlgorithm.IV, encryptedData); + } + + // Now get a signature for the data so we can detect tampering in situ. + byte[] signature = SignData( + cipherTextAndIV, + masterKey, + encryptingAlgorithm, + signingAlgorithm, + keyDerivationIterationCount); + + // Add the signature to the cipher text. + var signedData = CombineByteArrays(signature, cipherTextAndIV); + + // Add our algorithm identifier to the combined signature and cipher text. + var algorithmIdentifier = BitConverter.GetBytes((int)_defaultAlgorithm); + byte[] output = CombineByteArrays(algorithmIdentifier, signedData); + + // Clean everything up. + encryptingAlgorithm.Clear(); + signingAlgorithm.Clear(); + encryptingAlgorithm.Dispose(); + signingAlgorithm.Dispose(); + + Array.Clear(plainText, 0, plainText.Length); + + // Return the results as a string. + return Convert.ToBase64String(output); + } + + public string Unprotect(string keyId, string data) + { + var masterKey = MasterKey(keyId); + byte[] plainText; + + // Take our string and convert it back to bytes. + var payload = Convert.FromBase64String(data); + + // Read the saved algorithm details and create instances of those algorithms. + byte[] algorithmIdentifierAsBytes = new byte[4]; + Buffer.BlockCopy(payload, 0, algorithmIdentifierAsBytes, 0, 4); + var algorithmIdentifier = (ProtectorAlgorithm)(BitConverter.ToInt32(algorithmIdentifierAsBytes, 0)); + ProtectorAlgorithmHelper.GetAlgorithms( + _defaultAlgorithm, + out SymmetricAlgorithm encryptingAlgorithm, + out KeyedHashAlgorithm signingAlgorithm, + out int keyDerivationIterationCount); + + // Now extract the signature + byte[] signature = new byte[signingAlgorithm.HashSize / 8]; + Buffer.BlockCopy(payload, 4, signature, 0, signingAlgorithm.HashSize / 8); + + // And finally grab the rest of the data + var dataLength = payload.Length - 4 - signature.Length; + byte[] cipherTextAndIV = new byte[dataLength]; + Buffer.BlockCopy(payload, 4 + signature.Length, cipherTextAndIV, 0, dataLength); + + // Check the signature before anything else is done to detect tampering and avoid + // oracles. + byte[] computedSignature = SignData( + cipherTextAndIV, + masterKey, + encryptingAlgorithm, + signingAlgorithm, + keyDerivationIterationCount); + if (!ByteArraysEqual(computedSignature, signature)) + { + throw new CryptographicException(@"Invalid Signature."); + } + signingAlgorithm.Clear(); + signingAlgorithm.Dispose(); + + // The signature is valid, so now we can work on decrypting the data. + var ivLength = encryptingAlgorithm.BlockSize / 8; + byte[] initializationVector = new byte[ivLength]; + byte[] cipherText = new byte[cipherTextAndIV.Length - ivLength]; + // The IV is embedded in the cipher text, so we extract it out. + Buffer.BlockCopy(cipherTextAndIV, 0, initializationVector, 0, ivLength); + // Then we get the encrypted data. + Buffer.BlockCopy(cipherTextAndIV, ivLength, cipherText, 0, cipherTextAndIV.Length - ivLength); + + encryptingAlgorithm.Key = DerivedEncryptionKey( + masterKey, + encryptingAlgorithm, + keyDerivationIterationCount); + encryptingAlgorithm.IV = initializationVector; + + // Decrypt + using (var ms = new MemoryStream()) + using (var cs = new CryptoStream(ms, encryptingAlgorithm.CreateDecryptor(), CryptoStreamMode.Write)) + { + cs.Write(cipherText); + cs.FlushFinalBlock(); + plainText = ms.ToArray(); + } + encryptingAlgorithm.Clear(); + encryptingAlgorithm.Dispose(); + + // And convert from the bytes back to a string. + return Encoding.UTF8.GetString(plainText); + } + + public byte[] MasterKey(string keyId) + { + return Convert.FromBase64String(_keyRing[keyId]); + } + + private byte[] SignData(byte[] cipherText, byte[] masterKey, SymmetricAlgorithm symmetricAlgorithm, KeyedHashAlgorithm hashAlgorithm, int keyDerivationIterationCount) + { + hashAlgorithm.Key = DerivedSigningKey(masterKey, symmetricAlgorithm, keyDerivationIterationCount); + byte[] signature = hashAlgorithm.ComputeHash(cipherText); + hashAlgorithm.Clear(); + return signature; + } + + private byte[] DerivedSigningKey(byte[] key, SymmetricAlgorithm algorithm, int keyDerivationIterationCount) + { + return KeyDerivation.Pbkdf2( + @"IdentityLookupDataSigning", + key, + KeyDerivationPrf.HMACSHA512, + keyDerivationIterationCount, + algorithm.KeySize / 8); + } + + private byte[] DerivedEncryptionKey(byte[] key, SymmetricAlgorithm algorithm, int keyDerivationIterationCount) + { + return KeyDerivation.Pbkdf2( + @"IdentityLookupEncryption", + key, + KeyDerivationPrf.HMACSHA512, + keyDerivationIterationCount, + algorithm.KeySize / 8); + } + + private byte[] DerivedInitializationVector(byte[] key, string plainText, SymmetricAlgorithm algorithm, int keyDerivationIterationCount) + { + return KeyDerivation.Pbkdf2( + plainText, + key, + KeyDerivationPrf.HMACSHA512, + keyDerivationIterationCount, + algorithm.BlockSize / 8); + } + + private byte[] CombineByteArrays(byte[] left, byte[] right) + { + byte[] output = new byte[left.Length + right.Length]; + Buffer.BlockCopy(left, 0, output, 0, left.Length); + Buffer.BlockCopy(right, 0, output, left.Length, right.Length); + + return output; + } + + [MethodImpl(MethodImplOptions.NoOptimization)] + private static bool ByteArraysEqual(byte[] a, byte[] b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + if (a == null || b == null || a.Length != b.Length) + { + return false; + } + + bool areSame = true; + for (int i = 0; i < a.Length; i++) + { + areSame &= (a[i] == b[i]); + } + return areSame; + } + +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/PersonalDataProtector.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/PersonalDataProtector.cs new file mode 100644 index 0000000..9b24ba2 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/PersonalDataProtector.cs @@ -0,0 +1,241 @@ +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Infrastructure.Identity.Identity.DataProtection; + +public class PersonalDataProtector : IPersonalDataProtector +{ + private readonly ProtectorAlgorithm _defaultAlgorithm = ProtectorAlgorithm.Aes256Hmac512; + private readonly ILookupProtectorKeyRing _keyRing; + + public PersonalDataProtector(ILookupProtectorKeyRing keyRing) + { + _keyRing = keyRing; + } + + public string Protect(string data) + { + // Get the default algorithms. + // We does this so we can embed the algorithm details used in the cipher text so we can + // change algorithms as yet another collision appears in a hashing algorithm. + // See https://media.blackhat.com/bh-us-10/whitepapers/Sullivan/BlackHat-USA-2010-Sullivan-Cryptographic-Agility-wp.pdf + ProtectorAlgorithmHelper.GetAlgorithms( + _defaultAlgorithm, + out SymmetricAlgorithm encryptingAlgorithm, + out KeyedHashAlgorithm signingAlgorithm, + out int keyDerivationIterationCount); + + // Use the newest key from the keyring. + // We know this is a guid, because that's how our keyring works underneath, + // so we can prepend this later to the result. + string keyId = _keyRing.CurrentKeyId; + var masterKey = Key(keyId); + + // Convert the string to bytes, because encryption works on bytes, not strings. + var plainText = Encoding.UTF8.GetBytes(data); + byte[] cipherTextAndIV; + + // Derive a key for encryption from the master key + encryptingAlgorithm.Key = DerivedEncryptionKey( + masterKey, + encryptingAlgorithm, + keyDerivationIterationCount); + + // When the underlying encryption class is created it has a random IV by default + // So we don't need to do anything IV wise. + + // And encrypt + using (var ms = new MemoryStream()) + using (var cs = new CryptoStream( + ms, + encryptingAlgorithm.CreateEncryptor(), + CryptoStreamMode.Write)) + { + cs.Write(plainText); + cs.FlushFinalBlock(); + var encryptedData = ms.ToArray(); + + cipherTextAndIV = CombineByteArrays(encryptingAlgorithm.IV, encryptedData); + } + + // Now get a signature for the data so we can detect tampering in situ. + byte[] signature = SignData( + cipherTextAndIV, + masterKey, + encryptingAlgorithm, + signingAlgorithm, + keyDerivationIterationCount); + + // Add the signature to the cipher text. + var signedData = CombineByteArrays(signature, cipherTextAndIV); + + // Add our algorithm identifier to the combined signature and cipher text. + var algorithmIdentifier = BitConverter.GetBytes((int)_defaultAlgorithm); + byte[] dataPlusAlgorithmId = CombineByteArrays(algorithmIdentifier, signedData); + + // Now we need to put our key identifier in. In our implementation this is a GUID + // so let's convert it back to one, then turn it to bytes. + byte[] output = CombineByteArrays(Guid.Parse(keyId).ToByteArray(), dataPlusAlgorithmId); + + // Clean everything up. + encryptingAlgorithm.Clear(); + signingAlgorithm.Clear(); + encryptingAlgorithm.Dispose(); + signingAlgorithm.Dispose(); + + Array.Clear(plainText, 0, plainText.Length); + + // Return the results as a string. + return Convert.ToBase64String(output); + } + + public string Unprotect(string data) + { + byte[] plainText; + + // Take our string and convert it back to bytes. + var payload = Convert.FromBase64String(data); + + var offset = 0; + + // First we extract our key ID and then the appropriate key. + byte[] keyIdAsBytes = new byte[16]; + Buffer.BlockCopy(payload, offset, keyIdAsBytes, 0, 16); + var keyIdAsGuid = new Guid(keyIdAsBytes); + var keyId = keyIdAsGuid.ToString(); + var masterKey = Key(keyId); + offset = 16; + + // Next read the saved algorithm details and create instances of those algorithms. + byte[] algorithmIdentifierAsBytes = new byte[4]; + Buffer.BlockCopy(payload, offset, algorithmIdentifierAsBytes, 0, 4); + var algorithmIdentifier = (ProtectorAlgorithm)(BitConverter.ToInt32(algorithmIdentifierAsBytes, 0)); + ProtectorAlgorithmHelper.GetAlgorithms( + _defaultAlgorithm, + out SymmetricAlgorithm encryptingAlgorithm, + out KeyedHashAlgorithm signingAlgorithm, + out int keyDerivationIterationCount); + offset += 4; + + // Now extract the signature + byte[] signature = new byte[signingAlgorithm.HashSize / 8]; + Buffer.BlockCopy(payload, offset, signature, 0, signingAlgorithm.HashSize / 8); + offset += signature.Length; + + // And finally grab the rest of the data + var dataLength = payload.Length - offset; + byte[] cipherTextAndIV = new byte[dataLength]; + Buffer.BlockCopy(payload, offset, cipherTextAndIV, 0, dataLength); + + // Check the signature before anything else is done to detect tampering and avoid + // oracles. + byte[] computedSignature = SignData( + cipherTextAndIV, + masterKey, + encryptingAlgorithm, + signingAlgorithm, + keyDerivationIterationCount); + if (!ByteArraysEqual(computedSignature, signature)) + { + throw new CryptographicException(@"Invalid Signature."); + } + signingAlgorithm.Clear(); + signingAlgorithm.Dispose(); + + // The signature is valid, so now we can work on decrypting the data. + var ivLength = encryptingAlgorithm.BlockSize / 8; + byte[] initializationVector = new byte[ivLength]; + byte[] cipherText = new byte[cipherTextAndIV.Length - ivLength]; + // The IV is embedded in the cipher text, so we extract it out. + Buffer.BlockCopy(cipherTextAndIV, 0, initializationVector, 0, ivLength); + // Then we get the encrypted data. + Buffer.BlockCopy(cipherTextAndIV, ivLength, cipherText, 0, cipherTextAndIV.Length - ivLength); + + encryptingAlgorithm.Key = DerivedEncryptionKey( + masterKey, + encryptingAlgorithm, + keyDerivationIterationCount); + encryptingAlgorithm.IV = initializationVector; + + // Decrypt + using (var ms = new MemoryStream()) + using (var cs = new CryptoStream(ms, encryptingAlgorithm.CreateDecryptor(), CryptoStreamMode.Write)) + { + cs.Write(cipherText); + cs.FlushFinalBlock(); + plainText = ms.ToArray(); + } + encryptingAlgorithm.Clear(); + encryptingAlgorithm.Dispose(); + + // And convert from the bytes back to a string. + return Encoding.UTF8.GetString(plainText); + } + + public byte[] Key(string keyId) + { + return Convert.FromBase64String(_keyRing[keyId]); + } + + private byte[] SignData(byte[] cipherText, byte[] masterKey, SymmetricAlgorithm symmetricAlgorithm, KeyedHashAlgorithm hashAlgorithm, int keyDerivationIterationCount) + { + hashAlgorithm.Key = DerivedSigningKey(masterKey, symmetricAlgorithm, keyDerivationIterationCount); + byte[] signature = hashAlgorithm.ComputeHash(cipherText); + hashAlgorithm.Clear(); + return signature; + } + + private byte[] DerivedSigningKey(byte[] key, SymmetricAlgorithm algorithm, int keyDerivationIterationCount) + { + return KeyDerivation.Pbkdf2( + @"PersonalDataSigning", + key, + KeyDerivationPrf.HMACSHA512, + keyDerivationIterationCount, + algorithm.KeySize / 8); + } + + private byte[] DerivedEncryptionKey(byte[] key, SymmetricAlgorithm algorithm, int keyDerivationIterationCount) + { + return KeyDerivation.Pbkdf2( + @"PersonalDataEncryption", + key, + KeyDerivationPrf.HMACSHA512, + keyDerivationIterationCount, + algorithm.KeySize / 8); + } + + private byte[] CombineByteArrays(byte[] left, byte[] right) + { + byte[] output = new byte[left.Length + right.Length]; + Buffer.BlockCopy(left, 0, output, 0, left.Length); + Buffer.BlockCopy(right, 0, output, left.Length, right.Length); + + return output; + } + + [MethodImpl(MethodImplOptions.NoOptimization)] + private static bool ByteArraysEqual(byte[] a, byte[] b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + if (a == null || b == null || a.Length != b.Length) + { + return false; + } + + bool areSame = true; + for (int i = 0; i < a.Length; i++) + { + areSame &= (a[i] == b[i]); + } + return areSame; + } + +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/ProtectorAlgorithmHelper.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/ProtectorAlgorithmHelper.cs new file mode 100644 index 0000000..800cfac --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/ProtectorAlgorithmHelper.cs @@ -0,0 +1,35 @@ +using System.Security.Cryptography; + +namespace CleanArc.Infrastructure.Identity.Identity.DataProtection; + +public enum ProtectorAlgorithm +{ + Aes256Hmac512 = 1 +} + +public static class ProtectorAlgorithmHelper +{ + public static ProtectorAlgorithm DefaultAlgorithm + { + get { return ProtectorAlgorithm.Aes256Hmac512; } + } + + public static void GetAlgorithms( + ProtectorAlgorithm algorithmId, + out SymmetricAlgorithm encryptionAlgorithm, + out KeyedHashAlgorithm signingAlgorithm, + out int keyDerivationIterationCount) + { + switch (algorithmId) + { + case ProtectorAlgorithm.Aes256Hmac512: + encryptionAlgorithm = Aes.Create(); + encryptionAlgorithm.KeySize = 256; + signingAlgorithm = new HMACSHA512(); + keyDerivationIterationCount = 10000; + break; + default: + throw new ArgumentOutOfRangeException(nameof(algorithmId)); + } + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Dtos/CustomIdentityConstants.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Dtos/CustomIdentityConstants.cs new file mode 100644 index 0000000..fa12893 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Dtos/CustomIdentityConstants.cs @@ -0,0 +1,8 @@ +namespace CleanArc.Infrastructure.Identity.Identity.Dtos +{ + internal class CustomIdentityConstants + { + public const string OtpPasswordLessLoginProvider = "PasswordlessLoginTotpProvider"; + public const string OtpPasswordLessLoginPurpose = "passwordless-auth"; + } +} diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Dtos/IdentitySettings.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Dtos/IdentitySettings.cs new file mode 100644 index 0000000..83559af --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Dtos/IdentitySettings.cs @@ -0,0 +1,11 @@ +namespace CleanArc.Infrastructure.Identity.Identity.Dtos; + +public class IdentitySettings +{ + public string SecretKey { get; set; } + public string Encryptkey { get; set; } + public string Issuer { get; set; } + public string Audience { get; set; } + public int NotBeforeMinutes { get; set; } + public int ExpirationMinutes { get; set; } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Extensions/CustomIdentityExtensions.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Extensions/CustomIdentityExtensions.cs new file mode 100644 index 0000000..92d1ffe --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Extensions/CustomIdentityExtensions.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace CleanArc.Infrastructure.Identity.Identity.Extensions; + +public class DataProtectionTokenProviderOptions +{ + public string Name { get; set; } = "DataProtectorTokenProvider"; + public TimeSpan TokenLifespan { get; set; } +} + +public static class CustomIdentityExtensions +{ + public static IdentityBuilder AddPasswordlessLoginTotpTokenProvider(this IdentityBuilder builder) + { + var userType = builder.UserType; + var totpProvider = typeof(PasswordlessLoginTotpTokenProvider<>).MakeGenericType(userType); + return builder.AddTokenProvider("PasswordlessLoginTotpProvider", totpProvider); + } +} + +public class PasswordlessLoginTotpTokenProvider : TotpSecurityStampBasedTokenProvider + where TUser : class +{ + public override Task CanGenerateTwoFactorTokenAsync(UserManager manager, TUser user) + { + return Task.FromResult(false); + } + + public override async Task GetUserModifierAsync(string purpose, UserManager manager, TUser user) + { + var phone = await manager.GetPhoneNumberAsync(user); + return "PasswordlessLogin:" + purpose + ":" + phone; + } +} + +public class PasswordlessLoginTokenProviderOptions : DataProtectionTokenProviderOptions +{ + public PasswordlessLoginTokenProviderOptions() + { + // update the defaults + Name = "PasswordlessLoginTokenProvider"; + TokenLifespan = TimeSpan.FromMinutes(1); + } +} + +public class PasswordlessLoginTokenProvider : DataProtectorTokenProvider + where TUser : class +{ + public PasswordlessLoginTokenProvider(IDataProtectionProvider dataProtectionProvider, IOptions options, ILogger> logger) : base(dataProtectionProvider, options,logger) + { + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Manager/AppRoleManager.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Manager/AppRoleManager.cs new file mode 100644 index 0000000..3800894 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Manager/AppRoleManager.cs @@ -0,0 +1,12 @@ +using CleanArc.Domain.Entities.User; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; + +namespace CleanArc.Infrastructure.Identity.Identity.Manager; + +public class AppRoleManager:RoleManager +{ + public AppRoleManager(IRoleStore store, IEnumerable> roleValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, ILogger> logger) : base(store, roleValidators, keyNormalizer, errors, logger) + { + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Manager/AppSignInManager.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Manager/AppSignInManager.cs new file mode 100644 index 0000000..9f7b400 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Manager/AppSignInManager.cs @@ -0,0 +1,15 @@ +using CleanArc.Domain.Entities.User; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace CleanArc.Infrastructure.Identity.Identity.Manager; + +public class AppSignInManager : SignInManager +{ + public AppSignInManager(UserManager userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory claimsFactory, IOptions optionsAccessor, ILogger> logger, IAuthenticationSchemeProvider schemes, IUserConfirmation confirmation) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + { + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Manager/AppUserManager.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Manager/AppUserManager.cs new file mode 100644 index 0000000..24cd0a5 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Manager/AppUserManager.cs @@ -0,0 +1,13 @@ +using CleanArc.Domain.Entities.User; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace CleanArc.Infrastructure.Identity.Identity.Manager; + +public class AppUserManager:UserManager +{ + public AppUserManager(IUserStore store, IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + { + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/ConstantPolicies.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/ConstantPolicies.cs new file mode 100644 index 0000000..5200a92 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/ConstantPolicies.cs @@ -0,0 +1,6 @@ +namespace CleanArc.Infrastructure.Identity.Identity.PermissionManager; + +public static class ConstantPolicies +{ + public const string DynamicPermission = nameof(DynamicPermission); +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/DynamicPermissionRequirement.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/DynamicPermissionRequirement.cs new file mode 100644 index 0000000..d6ec152 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/DynamicPermissionRequirement.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace CleanArc.Infrastructure.Identity.Identity.PermissionManager; + +public class DynamicPermissionRequirement : IAuthorizationRequirement +{ +} + +public class DynamicPermissionHandler : AuthorizationHandler +{ + private readonly IDynamicPermissionService _dynamicPermissionService; + private readonly IHttpContextAccessor _contextAccessor; + + public DynamicPermissionHandler( + IDynamicPermissionService dynamicPermissionService, + IHttpContextAccessor contextAccessor + ) + { + _dynamicPermissionService = dynamicPermissionService; + _contextAccessor = contextAccessor; + } + + + + protected override Task HandleRequirementAsync( + AuthorizationHandlerContext context, + DynamicPermissionRequirement requirement) + { + + var user = _contextAccessor.HttpContext.User; + + var routeData = _contextAccessor.HttpContext.GetRouteData().Values; + + var controller = routeData["controller"].ToString(); + + var action = routeData["action"].ToString(); + + var area = routeData["area"]?.ToString(); + + if (_dynamicPermissionService.CanAccess(user, area, controller, action)) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/DynamicPermissionService.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/DynamicPermissionService.cs new file mode 100644 index 0000000..3bc0b93 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/DynamicPermissionService.cs @@ -0,0 +1,23 @@ +using System.Security.Claims; + +namespace CleanArc.Infrastructure.Identity.Identity.PermissionManager; + +public class DynamicPermissionService : IDynamicPermissionService +{ + public bool CanAccess(ClaimsPrincipal user, string area, string controller, string action) + { + if (user.IsInRole("admin")) + { + return true; + } + + + var key = $"{area}:{controller}:"; + + var userClaims = user.FindAll(ConstantPolicies.DynamicPermission); + + return userClaims.Any(item => item.Value.Equals(key, StringComparison.OrdinalIgnoreCase)); + } + + +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/IDynamicPermissionService.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/IDynamicPermissionService.cs new file mode 100644 index 0000000..71370c8 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/IDynamicPermissionService.cs @@ -0,0 +1,8 @@ +using System.Security.Claims; + +namespace CleanArc.Infrastructure.Identity.Identity.PermissionManager; + +public interface IDynamicPermissionService +{ + bool CanAccess(ClaimsPrincipal user, string area, string controller, string action); +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/RoleManagerService.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/RoleManagerService.cs new file mode 100644 index 0000000..0af1e0b --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/PermissionManager/RoleManagerService.cs @@ -0,0 +1,214 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Models.Identity; +using CleanArc.Domain.Entities.User; +using CleanArc.Infrastructure.Identity.Identity.Manager; +using CleanArc.Infrastructure.Persistence; +using MapsterMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace CleanArc.Infrastructure.Identity.Identity.PermissionManager; + +internal class RoleManagerService : IRoleManagerService +{ + private readonly AppRoleManager _roleManger; + private readonly IMapper _mapper; + private readonly IActionDescriptorCollectionProvider _actionDescriptor; + private readonly EndpointDataSource _endpointDataSource; + private readonly AppUserManager _userManager; + private readonly ILogger _logger; + private readonly ApplicationDbContext _db; + + public RoleManagerService(AppRoleManager roleManger, IMapper mapper, IActionDescriptorCollectionProvider actionDescriptor, AppUserManager userManager, ILogger logger, ApplicationDbContext db, EndpointDataSource endpointDataSource) + { + _roleManger = roleManger; + _mapper = mapper; + _actionDescriptor = actionDescriptor; + _userManager = userManager; + _logger = logger; + _db = db; + _endpointDataSource = endpointDataSource; + } + + public async Task> GetRolesAsync() + { + var result = await _roleManger.Roles.Where(c => !c.Name.Equals("admin")).Select(r => _mapper.Map(r)).ToListAsync(); + return result; + } + + public async Task CreateRoleAsync(CreateRoleDto model) + { + var role = new Role + { + Name = model.RoleName, + }; + + var result = await _roleManger.CreateAsync(role); + + return result; + } + + public async Task DeleteRoleAsync(int roleId) + { + var role = await _roleManger.Roles.Include(r => r.Claims) + .Include(r => r.Users).FirstOrDefaultAsync(r => r.Id == roleId); + + if (role == null) + return false; + + var users = await _userManager.GetUsersInRoleAsync(role.Name); + + foreach (var user in users) + { + await _userManager.RemoveFromRoleAsync(user, role.Name); + await _userManager.UpdateSecurityStampAsync(user); + } + + _db.RemoveRange(role.Claims); + _db.RemoveRange(role.Users); + _db.Remove(role); + await _db.SaveChangesAsync(); + + return true; + } + + public Task> GetPermissionActionsAsync() + { + + var actions = new List(); + + var actionDescriptors = _actionDescriptor.ActionDescriptors.Items; + string controllerName = ""; + foreach (var actionDescriptor in actionDescriptors) + { + try + { + var descriptor = (ControllerActionDescriptor)actionDescriptor; + + var hasPermission = descriptor.ControllerTypeInfo.GetCustomAttribute()? + .Policy == ConstantPolicies.DynamicPermission; //|| + // descriptor.MethodInfo.GetCustomAttribute()? + // .Policy == ConstantPolicies.DynamicPermission; + + + + if (hasPermission && (controllerName != descriptor.ControllerName)) + { + actions.Add(new ActionDescriptionDto + { + // ActionName = descriptor.ActionName, + ControllerName = descriptor.ControllerName, + ActionDisplayName = descriptor.MethodInfo.GetCustomAttribute()?.Name, + AreaName = descriptor.ControllerTypeInfo.GetCustomAttribute()?.RouteValue, + ControllerDisplayName = descriptor.ControllerTypeInfo.GetCustomAttribute() + ?.Name, + ControllerDescription = descriptor.ControllerTypeInfo.GetCustomAttribute() + ?.Description + }); + controllerName = descriptor.ControllerName; + } + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + } + } + + return Task.FromResult(actions); + } + + public async Task GetRolePermissionsAsync(int roleId) + { + var role = await _roleManger.Roles + .Include(x => x.Claims) + .SingleOrDefaultAsync(x => x.Id == roleId); + + if (role == null) + return null; + + var dynamicActions = await this.GetPermissionActionsAsync(); + return new RolePermissionDto + { + Role = role, + Actions = dynamicActions + }; + } + + public async Task ChangeRolePermissionsAsync(EditRolePermissionsDto model) + { + var role = await _roleManger.Roles + .Include(x => x.Claims) + .SingleOrDefaultAsync(x => x.Id == model.RoleId); + + if (role == null) + { + return false; + } + + var selectedPermissions = model.Permissions; + + var roleClaims = role.Claims + .Where(x => x.ClaimType == ConstantPolicies.DynamicPermission) + .Select(x => x.ClaimValue) + .ToList(); + + + // add new permissions + var newPermissions = selectedPermissions.Except(roleClaims).ToList(); + foreach (var permission in newPermissions) + { + role.Claims.Add(new RoleClaim + { + ClaimType = ConstantPolicies.DynamicPermission, + ClaimValue = permission, + CreatedClaim = DateTime.Now, + RoleId = role.Id + }); + } + + // remove deleted permissions + var removedPermissions = roleClaims.Except(selectedPermissions).ToList(); + foreach (var permission in removedPermissions) + { + var roleClaim = role.Claims + .SingleOrDefault(x => + x.ClaimType == ConstantPolicies.DynamicPermission && + x.ClaimValue == permission); + + if (roleClaim != null) + { + _db.RemoveRange(roleClaim); + } + } + await _db.SaveChangesAsync(); + + var result = await _roleManger.UpdateAsync(role); + + if (result.Succeeded) + { + var users = await _userManager.GetUsersInRoleAsync(role.Name); + + foreach (var user in users) + { + await _userManager.UpdateSecurityStampAsync(user); + } + + return true; + } + + return false; + } + + public async Task GetRoleByIdAsync(int roleId) + { + return await _roleManger.FindByIdAsync(roleId.ToString()); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/SeedDatabaseService/SeedDataBase.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/SeedDatabaseService/SeedDataBase.cs new file mode 100644 index 0000000..ce67221 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/SeedDatabaseService/SeedDataBase.cs @@ -0,0 +1,47 @@ +using CleanArc.Domain.Entities.User; +using CleanArc.Infrastructure.Identity.Identity.Manager; +using Microsoft.EntityFrameworkCore; + +namespace CleanArc.Infrastructure.Identity.Identity.SeedDatabaseService; + +public interface ISeedDataBase +{ + Task Seed(); +} + +public class SeedDataBase : ISeedDataBase +{ + private readonly AppUserManager _userManager; + private readonly AppRoleManager _roleManager; + + public SeedDataBase(AppUserManager userManager, AppRoleManager roleManager) + { + _userManager = userManager; + _roleManager = roleManager; + } + + public async Task Seed() + { + if (!_roleManager.Roles.AsNoTracking().Any(r => r.Name.Equals("admin"))) + { + var role=new Role + { + Name = "admin", + }; + await _roleManager.CreateAsync(role); + } + + if (!_userManager.Users.AsNoTracking().Any(u => u.UserName.Equals("admin"))) + { + var user = new User + { + UserName = "admin", + Email = "admin@site.com", + PhoneNumberConfirmed = true + }; + + await _userManager.CreateAsync(user, "qw123321"); + await _userManager.AddToRoleAsync(user,"admin"); + } + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Store/AppUserStore.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Store/AppUserStore.cs new file mode 100644 index 0000000..c63f436 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Store/AppUserStore.cs @@ -0,0 +1,13 @@ +using CleanArc.Domain.Entities.User; +using CleanArc.Infrastructure.Persistence; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; + +namespace CleanArc.Infrastructure.Identity.Identity.Store; + +public class AppUserStore:UserStore +{ + public AppUserStore(ApplicationDbContext context, IdentityErrorDescriber describer = null) : base(context, describer) + { + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Store/RoleStore.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Store/RoleStore.cs new file mode 100644 index 0000000..f214974 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/Store/RoleStore.cs @@ -0,0 +1,13 @@ +using CleanArc.Domain.Entities.User; +using CleanArc.Infrastructure.Persistence; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; + +namespace CleanArc.Infrastructure.Identity.Identity.Store; + +public class RoleStore:RoleStore +{ + public RoleStore(ApplicationDbContext context, IdentityErrorDescriber describer = null) : base(context, describer) + { + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/validator/AppRoleValidator.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/validator/AppRoleValidator.cs new file mode 100644 index 0000000..e7d072f --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/validator/AppRoleValidator.cs @@ -0,0 +1,17 @@ +using CleanArc.Domain.Entities.User; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Infrastructure.Identity.Identity.validator; + +public class AppRoleValidator:RoleValidator +{ + public AppRoleValidator(IdentityErrorDescriber errors):base(errors) + { + + } + + public override Task ValidateAsync(RoleManager manager, Role role) + { + return base.ValidateAsync(manager, role); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/validator/AppUserValidator.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/validator/AppUserValidator.cs new file mode 100644 index 0000000..fde0c30 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/validator/AppUserValidator.cs @@ -0,0 +1,21 @@ +using CleanArc.Domain.Entities.User; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.Infrastructure.Identity.Identity.validator; + +public class AppUserValidator:UserValidator +{ + public AppUserValidator(IdentityErrorDescriber errors):base(errors) + { + + } + + public override async Task ValidateAsync(UserManager manager, User user) + { + var result=await base.ValidateAsync(manager, user); + + return result; + } + + +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Jwt/JwtService.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Jwt/JwtService.cs new file mode 100644 index 0000000..59bfbe8 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Jwt/JwtService.cs @@ -0,0 +1,122 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using CleanArc.Application.Contracts; +using CleanArc.Application.Contracts.Persistence; +using CleanArc.Application.Models.Jwt; +using CleanArc.Domain.Entities.User; +using CleanArc.Infrastructure.Identity.Identity.Dtos; +using CleanArc.Infrastructure.Identity.Identity.Manager; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace CleanArc.Infrastructure.Identity.Jwt; + +public class JwtService : IJwtService +{ + private readonly IdentitySettings _siteSetting; + private readonly AppUserManager _userManager; + private IUserClaimsPrincipalFactory _claimsPrincipal; + + private readonly IUnitOfWork _unitOfWork; + //private readonly AppUserClaimsPrincipleFactory claimsPrincipleFactory; + + public JwtService(IOptions siteSetting, AppUserManager userManager, IUserClaimsPrincipalFactory claimsPrincipal, IUnitOfWork unitOfWork) + { + _siteSetting = siteSetting.Value; + _userManager = userManager; + _claimsPrincipal = claimsPrincipal; + _unitOfWork = unitOfWork; + } + public async Task GenerateAsync(User user) + { + var secretKey = Encoding.UTF8.GetBytes(_siteSetting.SecretKey); // longer that 16 character + var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature); + + var encryptionkey = Encoding.UTF8.GetBytes(_siteSetting.Encryptkey); //must be 16 character + var encryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(encryptionkey), SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256); + + + var claims = await _getClaimsAsync(user); + + var descriptor = new SecurityTokenDescriptor + { + Issuer = _siteSetting.Issuer, + Audience = _siteSetting.Audience, + IssuedAt = DateTime.Now, + NotBefore = DateTime.Now.AddMinutes(0), + Expires = DateTime.Now.AddMinutes(_siteSetting.ExpirationMinutes), + SigningCredentials = signingCredentials, + EncryptingCredentials = encryptingCredentials, + Subject = new ClaimsIdentity(claims) + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + + var securityToken = tokenHandler.CreateJwtSecurityToken(descriptor); + + + var refreshToken = await _unitOfWork.UserRefreshTokenRepository.CreateToken(user.Id); + await _unitOfWork.CommitAsync(); + + return new AccessToken(securityToken,refreshToken.ToString()); + } + + public Task GetPrincipalFromExpiredToken(string token) + { + var tokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, //you might want to validate the audience and issuer depending on your use case + ValidateIssuer = false, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_siteSetting.SecretKey)), + ValidateLifetime = false, + TokenDecryptionKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_siteSetting.Encryptkey)) + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + SecurityToken securityToken; + var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken); + var jwtSecurityToken = securityToken as JwtSecurityToken; + if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) + throw new SecurityTokenException("Invalid token"); + + return Task.FromResult(principal); + } + + public async Task GenerateByPhoneNumberAsync(string phoneNumber) + { + var user = await _userManager.Users.AsNoTracking().FirstOrDefaultAsync(u => u.PhoneNumber == phoneNumber); + var result = await this.GenerateAsync(user); + return result; + } + + public async Task RefreshToken(Guid refreshTokenId) + { + var refreshToken = await _unitOfWork.UserRefreshTokenRepository.GetTokenWithInvalidation(refreshTokenId); + + if (refreshToken is null) + return null; + + refreshToken.IsValid = false; + + await _unitOfWork.CommitAsync(); + + var user = await _unitOfWork.UserRefreshTokenRepository.GetUserByRefreshToken(refreshTokenId); + + if (user is null) + return null; + + var result = await this.GenerateAsync(user); + + return result; + } + + private async Task> _getClaimsAsync(User user) + { + var result = await _claimsPrincipal.CreateAsync(user); + return result.Claims; + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/ServiceConfiguration/ServiceCollectionExtension.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/ServiceConfiguration/ServiceCollectionExtension.cs new file mode 100644 index 0000000..9d757fa --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/ServiceConfiguration/ServiceCollectionExtension.cs @@ -0,0 +1,224 @@ +using System.Security.Claims; +using System.Text; +using CleanArc.Application.Contracts; +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Models.ApiResult; +using CleanArc.Domain.Entities.User; +using CleanArc.Infrastructure.Identity.Identity; +using CleanArc.Infrastructure.Identity.Identity.Dtos; +using CleanArc.Infrastructure.Identity.Identity.Extensions; +using CleanArc.Infrastructure.Identity.Identity.Manager; +using CleanArc.Infrastructure.Identity.Identity.PermissionManager; +using CleanArc.Infrastructure.Identity.Identity.SeedDatabaseService; +using CleanArc.Infrastructure.Identity.Identity.Store; +using CleanArc.Infrastructure.Identity.Identity.validator; +using CleanArc.Infrastructure.Identity.Jwt; +using CleanArc.Infrastructure.Identity.UserManager; +using CleanArc.SharedKernel.Extensions; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; + +namespace CleanArc.Infrastructure.Identity.ServiceConfiguration; + +public static class ServiceCollectionExtension +{ + public static IServiceCollection RegisterIdentityServices(this IServiceCollection services,IdentitySettings identitySettings) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped, AppUserValidator>(); + services.AddScoped, AppUserValidator>(); + + services.AddScoped, AppUserClaimsPrincipleFactory>(); + + services.AddScoped, AppRoleValidator>(); + services.AddScoped, AppRoleValidator>(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped, RoleStore>(); + services.AddScoped, AppUserStore>(); + services.AddScoped(); + + + services.AddIdentity(options => + { + options.Stores.ProtectPersonalData = false; + + options.Password.RequireDigit = false; + options.Password.RequireLowercase = false; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequiredUniqueChars = 0; + options.Password.RequireUppercase = false; + + options.SignIn.RequireConfirmedEmail = false; + options.SignIn.RequireConfirmedPhoneNumber = true; + + options.Lockout.MaxFailedAccessAttempts = 5; + options.Lockout.AllowedForNewUsers = false; + options.User.RequireUniqueEmail = false; + + //options.Stores.ProtectPersonalData = true; + + + }).AddUserStore() + .AddRoleStore(). + //.AddUserValidator(). + //AddRoleValidator(). + AddUserManager(). + AddRoleManager(). + AddErrorDescriber() + //.AddClaimsPrincipalFactory() + .AddDefaultTokenProviders(). + AddSignInManager() + .AddDefaultTokenProviders() + .AddPasswordlessLoginTotpTokenProvider(); + + + //For [ProtectPersonalData] Attribute In Identity + + //services.AddScoped(); + + //services.AddScoped(); + + //services.AddScoped(); + + services.AddAuthorization(options => + { + options.AddPolicy(ConstantPolicies.DynamicPermission, policy => + { + policy.RequireAuthenticatedUser(); + policy.Requirements.Add(new DynamicPermissionRequirement()); + }); + }); + + services.AddAuthentication( options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + + }).AddJwtBearer(options => + { + var secretkey = Encoding.UTF8.GetBytes(identitySettings.SecretKey); + var encryptionkey = Encoding.UTF8.GetBytes(identitySettings.Encryptkey); + + var validationParameters = new TokenValidationParameters + { + ClockSkew = TimeSpan.Zero, // default: 5 min + RequireSignedTokens = true, + + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(secretkey), + + RequireExpirationTime = true, + ValidateLifetime = true, + + ValidateAudience = true, //default : false + ValidAudience = identitySettings.Audience, + + ValidateIssuer = true, //default : false + ValidIssuer = identitySettings.Issuer, + + TokenDecryptionKey = new SymmetricSecurityKey(encryptionkey), + + }; + + options.RequireHttpsMetadata = false; + options.SaveToken = true; + options.TokenValidationParameters = validationParameters; + options.Events = new JwtBearerEvents + { + OnAuthenticationFailed = context => + { + //var logger = context.HttpContext.RequestServices.GetRequiredService().CreateLogger(nameof(JwtBearerEvents)); + //logger.LogError("Authentication failed.", context.Exception); + + return Task.CompletedTask; + }, + OnTokenValidated = async context => + { + var signInManager = context.HttpContext.RequestServices.GetRequiredService(); + + var claimsIdentity = context.Principal.Identity as ClaimsIdentity; + if (claimsIdentity.Claims?.Any() != true) + context.Fail("This token has no claims."); + + var securityStamp = + claimsIdentity.FindFirstValue(new ClaimsIdentityOptions().SecurityStampClaimType); + if (!securityStamp.HasValue()) + context.Fail("This token has no secuirty stamp"); + + //Find user and token from database and perform your custom validation + var userId = claimsIdentity.GetUserId(); + // var user = await userRepository.GetByIdAsync(context.HttpContext.RequestAborted, userId); + + //if (user.SecurityStamp != Guid.Parse(securityStamp)) + // context.Fail("Token secuirty stamp is not valid."); + + var validatedUser = await signInManager.ValidateSecurityStampAsync(context.Principal); + if (validatedUser == null) + context.Fail("Token secuirty stamp is not valid."); + + }, + OnChallenge = async context => + { + //var logger = context.HttpContext.RequestServices.GetRequiredService().CreateLogger(nameof(JwtBearerEvents)); + //logger.LogError("OnChallenge error", context.Error, context.ErrorDescription); + if (context.AuthenticateFailure is SecurityTokenExpiredException) + { + context.HandleResponse(); + + var response = new ApiResult(false, + ApiResultStatusCode.UnAuthorized, "Token is expired. refresh your token"); + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + await context.Response.WriteAsJsonAsync(response); + } + + else if (context.AuthenticateFailure != null) + { + context.HandleResponse(); + + var response = new ApiResult(false, + ApiResultStatusCode.UnAuthorized, "Token is Not Valid"); + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + await context.Response.WriteAsJsonAsync(response); + + } + + else + { + context.HandleResponse(); + + context.Response.StatusCode = (int)StatusCodes.Status401Unauthorized; + await context.Response.WriteAsJsonAsync(new ApiResult(false, ApiResultStatusCode.UnAuthorized, "Invalid access token. Please login")); + } + + }, + OnForbidden =async context => + { + context.Response.StatusCode = (int)StatusCodes.Status403Forbidden; + await context.Response.WriteAsJsonAsync(new ApiResult(false, + ApiResultStatusCode.Forbidden, ApiResultStatusCode.Forbidden.ToDisplay())); + } + }; + }); + + return services; + } + + public static async Task SeedDefaultUsersAsync(this WebApplication app) + { + await using var scope = app.Services.CreateAsyncScope(); + + var seedService = scope.ServiceProvider.GetRequiredService(); + await seedService.Seed(); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Identity/UserManager/AppUserManagerImplementation.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/UserManager/AppUserManagerImplementation.cs new file mode 100644 index 0000000..5550d2e --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Identity/UserManager/AppUserManagerImplementation.cs @@ -0,0 +1,145 @@ +using CleanArc.Application.Contracts.Identity; +using CleanArc.Domain.Entities.User; +using CleanArc.Infrastructure.Identity.Identity.Dtos; +using CleanArc.Infrastructure.Identity.Identity.Manager; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace CleanArc.Infrastructure.Identity.UserManager; + +public class AppUserManagerImplementation : IAppUserManager +{ + private readonly AppUserManager _userManager; + public AppUserManagerImplementation(AppUserManager userManager) + { + _userManager = userManager; + } + + public Task CreateUser(User user) + { + return _userManager.CreateAsync(user); + } + + public async Task CreateUser(User user, string password) + { + return await _userManager.CreateAsync(user, password); + } + + public Task IsExistUser(string phoneNumber) + { + return _userManager.Users.AnyAsync(c => c.PhoneNumber == phoneNumber); + } + + public Task IsExistUserName(string userName) + { + return _userManager.Users.AnyAsync(c => c.UserName.Equals(userName)); + } + + public async Task GeneratePhoneNumberConfirmationToken(User user, string phoneNumber) + { + return await _userManager.GenerateChangePhoneNumberTokenAsync(user, phoneNumber); + } + + public Task GetUserByCode(string code) + { + return _userManager.Users.FirstOrDefaultAsync(c => c.GeneratedCode.Equals(code)); + } + + public async Task ChangePhoneNumber(User user, string phoneNumber, string code) + { + return await _userManager.ChangePhoneNumberAsync(user, phoneNumber, code); + } + + public async Task VerifyUserCode(User user, string code) + { + var confirmationResult = await _userManager.VerifyUserTokenAsync( + user, CustomIdentityConstants.OtpPasswordLessLoginProvider, CustomIdentityConstants.OtpPasswordLessLoginPurpose, code); + + if (confirmationResult) + await _userManager.UpdateSecurityStampAsync(user); + + return confirmationResult + ? IdentityResult.Success + : IdentityResult.Failed(new IdentityError() { Description = "Incorrect Code" }); + } + + public Task GenerateOtpCode(User user) + { + return _userManager.GenerateUserTokenAsync( + user, CustomIdentityConstants.OtpPasswordLessLoginProvider, CustomIdentityConstants.OtpPasswordLessLoginPurpose); + } + + public Task GetUserByPhoneNumber(string phoneNumber) + { + return _userManager.Users.FirstOrDefaultAsync(c => c.PhoneNumber.Equals(phoneNumber)); + } + + + public Task GetByUserName(string userName) + { + return _userManager.FindByNameAsync(userName); + } + + public async Task GetUserByIdAsync(int userId) + { + return await _userManager.FindByIdAsync(userId.ToString()); + } + + public async Task> GetAllUsersAsync() + { + return await _userManager.Users.AsNoTracking().ToListAsync(); + } + + public async Task CreateUserWithPasswordAsync(User user, string password) + { + return await _userManager.CreateAsync(user, password); + } + + public async Task AddUserToRoleAsync(User user, Role role) + { + ArgumentNullException.ThrowIfNull(role, nameof(role)); + ArgumentNullException.ThrowIfNull(role.Name, nameof(role.Name)); + + return await _userManager.AddToRoleAsync(user, role.Name); + } + + public async Task IncrementAccessFailedCountAsync(User user) + { + + + return await _userManager.AccessFailedAsync(user); + } + + public async Task IsUserLockedOutAsync(User user) + { + var lockoutEndDate = await _userManager.GetLockoutEndDateAsync(user); + + return (lockoutEndDate.HasValue && lockoutEndDate.Value > DateTimeOffset.Now); + } + + public async Task ResetUserLockoutAsync(User user) + { + await _userManager.SetLockoutEndDateAsync(user, null); + await _userManager.ResetAccessFailedCountAsync(user); + } + + public async Task UpdateUserAsync(User user) + { + await _userManager.UpdateAsync(user); + } + + public async Task UpdateSecurityStampAsync(User user) + { + await _userManager.UpdateSecurityStampAsync(user); + } + + public async Task IsPasswordValidAsync(User user, string password) + { + return await _userManager.CheckPasswordAsync(user, password); + } + + public async Task GetRoleAsync(User user) + { + return (await _userManager.GetRolesAsync(user)).ToArray(); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/CleanArc.Infrastructure.Monitoring.csproj b/server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/CleanArc.Infrastructure.Monitoring.csproj new file mode 100644 index 0000000..b691635 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/CleanArc.Infrastructure.Monitoring.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/Configurations/HealthCheckConfigurations.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/Configurations/HealthCheckConfigurations.cs new file mode 100644 index 0000000..e65735c --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/Configurations/HealthCheckConfigurations.cs @@ -0,0 +1,38 @@ +using System.Net.Security; +using HealthChecks.UI.Client; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Prometheus; + +namespace CleanArc.Infrastructure.Monitoring.Configurations; + +public static class HealthCheckConfigurations +{ + public static WebApplicationBuilder ConfigureHealthChecks(this WebApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + .AddSqlServer(builder.Configuration.GetConnectionString("SqlServer")!, name: "SQL Server") + .ForwardToPrometheus(); + + var currentUrl = builder.Configuration["ASPNETCORE_URLS"]?.Split(';')[0].Replace("+", "localhost"); + + + return builder; + } + + public static WebApplication UseHealthChecks(this WebApplication app) + { + app.MapHealthChecks("/HealthCheck", new HealthCheckOptions + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }).ShortCircuit().DisableHttpMetrics(); + + + + return app; + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/Configurations/OpenTelemetryConfigurations.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/Configurations/OpenTelemetryConfigurations.cs new file mode 100644 index 0000000..5188609 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/Configurations/OpenTelemetryConfigurations.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Metrics; + +namespace CleanArc.Infrastructure.Monitoring.Configurations; + +public static class OpenTelemetryConfigurations +{ + public static WebApplicationBuilder SetupOpenTelemetry(this WebApplicationBuilder builder) + { + builder.Services.AddOpenTelemetry() + .WithMetrics(metricsBuilder => + { + metricsBuilder.AddRuntimeInstrumentation() + .AddAspNetCoreInstrumentation() + .AddMeter("Microsoft.AspNetCore.Hosting" + , "Microsoft.AspNetCore.Server.Kestrel" + , "System.Net.Http" + , "CleanArc.Web.Api" + , "ControllerMetrics") + .AddPrometheusExporter(); + }); + + builder.Services.AddMetrics(); + + return builder; + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/Configurations/PrometheusMetricsConfigurations.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/Configurations/PrometheusMetricsConfigurations.cs new file mode 100644 index 0000000..f4b8f14 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Monitoring/Configurations/PrometheusMetricsConfigurations.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Builder; +using Prometheus; + +namespace CleanArc.Infrastructure.Monitoring.Configurations; + +public static class PrometheusMetricsConfigurations +{ + public static WebApplication UseMetrics(this WebApplication app) + { + + app.UseMetricServer(); + app.UseHttpMetrics(); + return app; + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/ApplicationDbContext.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/ApplicationDbContext.cs new file mode 100644 index 0000000..1d936ee --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/ApplicationDbContext.cs @@ -0,0 +1,91 @@ +using System.Reflection; +using CleanArc.Domain.Common; +using CleanArc.Domain.Entities.User; +using CleanArc.SharedKernel.Extensions; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace CleanArc.Infrastructure.Persistence; + +public class ApplicationDbContext: IdentityDbContext +{ + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + base.SavingChanges += OnSavingChanges; + } + + private void OnSavingChanges(object sender, SavingChangesEventArgs e) + { + _cleanString(); + ConfigureEntityDates(); + } + + private void _cleanString() + { + var changedEntities = ChangeTracker.Entries() + .Where(x => x.State == EntityState.Added || x.State == EntityState.Modified); + foreach (var item in changedEntities) + { + if (item.Entity == null) + continue; + + var properties = item.Entity.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && p.CanWrite && p.PropertyType == typeof(string)); + + foreach (var property in properties) + { + var propName = property.Name; + var val = (string)property.GetValue(item.Entity, null); + + if (val.HasValue()) + { + var newVal = val.Fa2En().FixPersianChars(); + if (newVal == val) + continue; + property.SetValue(item.Entity, newVal, null); + } + } + } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + + base.OnModelCreating(modelBuilder); + + var entitiesAssembly = typeof(IEntity).Assembly; + modelBuilder.RegisterAllEntities(entitiesAssembly); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly); + modelBuilder.AddRestrictDeleteBehaviorConvention(); + modelBuilder.AddPluralizingTableNameConvention(); + + + } + + private void ConfigureEntityDates() + { + var updatedEntities = ChangeTracker.Entries().Where(x => + x.Entity is ITimeModification && x.State == EntityState.Modified).Select(x => x.Entity as ITimeModification); + + var addedEntities = ChangeTracker.Entries().Where(x => + x.Entity is ITimeModification && x.State == EntityState.Added).Select(x => x.Entity as ITimeModification); + + foreach (var entity in updatedEntities) + { + if (entity != null) + { + entity.ModifiedDate = DateTime.Now; + } + } + + foreach (var entity in addedEntities) + { + if (entity != null) + { + entity.CreatedTime = DateTime.Now; + entity.ModifiedDate = DateTime.Now; + } + } + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/CleanArc.Infrastructure.Persistence.csproj b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/CleanArc.Infrastructure.Persistence.csproj new file mode 100644 index 0000000..8ca61a0 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/CleanArc.Infrastructure.Persistence.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/OrderConfig/OrderConfig.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/OrderConfig/OrderConfig.cs new file mode 100644 index 0000000..d3a23a2 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/OrderConfig/OrderConfig.cs @@ -0,0 +1,15 @@ +using CleanArc.Domain.Entities.Order; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CleanArc.Infrastructure.Persistence.Configuration.OrderConfig; + +internal class OrderConfig:IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasOne(c => c.User).WithMany(c => c.Orders).HasForeignKey(c => c.UserId); + + builder.HasQueryFilter(c => !c.IsDeleted); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/RefreshTokenConfig.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/RefreshTokenConfig.cs new file mode 100644 index 0000000..5860298 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/RefreshTokenConfig.cs @@ -0,0 +1,15 @@ +using CleanArc.Domain.Entities.User; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CleanArc.Infrastructure.Persistence.Configuration.UserConfig; + +internal class RefreshTokenConfig:IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasOne(c => c.User).WithMany(c => c.UserRefreshTokens).HasForeignKey(c => c.UserId); + + builder.ToTable("UserRefreshTokens", "usr"); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/RoleClaimConfig.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/RoleClaimConfig.cs new file mode 100644 index 0000000..7f081f7 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/RoleClaimConfig.cs @@ -0,0 +1,17 @@ +using CleanArc.Domain.Entities.User; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CleanArc.Infrastructure.Persistence.Configuration.UserConfig; + +internal class RoleClaimConfig:IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("RoleClaims","usr"); + + builder.HasOne(u => u.Role).WithMany(u => u.Claims).HasForeignKey(u => u.RoleId).OnDelete(DeleteBehavior.Cascade); + + + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/RoleConfig.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/RoleConfig.cs new file mode 100644 index 0000000..3ca901d --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/RoleConfig.cs @@ -0,0 +1,13 @@ +using CleanArc.Domain.Entities.User; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CleanArc.Infrastructure.Persistence.Configuration.UserConfig; + +internal class RoleConfig:IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Roles","usr"); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserClaimConfig.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserClaimConfig.cs new file mode 100644 index 0000000..0bf13d5 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserClaimConfig.cs @@ -0,0 +1,15 @@ +using CleanArc.Domain.Entities.User; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CleanArc.Infrastructure.Persistence.Configuration.UserConfig; + +internal class UserClaimConfig:IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + + builder.HasOne(u => u.User).WithMany(u => u.Claims).HasForeignKey(u => u.UserId); + builder.ToTable("UserClaims","usr"); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserConfig.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserConfig.cs new file mode 100644 index 0000000..72b1431 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserConfig.cs @@ -0,0 +1,13 @@ +using CleanArc.Domain.Entities.User; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CleanArc.Infrastructure.Persistence.Configuration.UserConfig; + +internal class UserConfig:IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Users","usr").Property(p => p.Id).HasColumnName("UserId"); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserLoginConfig.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserLoginConfig.cs new file mode 100644 index 0000000..1208a37 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserLoginConfig.cs @@ -0,0 +1,14 @@ +using CleanArc.Domain.Entities.User; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CleanArc.Infrastructure.Persistence.Configuration.UserConfig; + +internal class UserLoginConfig:IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasOne(u => u.User).WithMany(u => u.Logins).HasForeignKey(u => u.UserId); + builder.ToTable("UserLogins","usr"); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserRoleConfig.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserRoleConfig.cs new file mode 100644 index 0000000..8ccbadb --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserRoleConfig.cs @@ -0,0 +1,16 @@ +using CleanArc.Domain.Entities.User; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CleanArc.Infrastructure.Persistence.Configuration.UserConfig; + +internal class UserRoleConfig:IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + + builder.HasOne(u => u.User).WithMany(u => u.UserRoles).HasForeignKey(u => u.UserId); + builder.HasOne(u => u.Role).WithMany(u => u.Users).HasForeignKey(u => u.RoleId); + builder.ToTable("UserRoles","usr"); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserTokenConfig.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserTokenConfig.cs new file mode 100644 index 0000000..2c4f415 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Configuration/UserConfig/UserTokenConfig.cs @@ -0,0 +1,15 @@ +using CleanArc.Domain.Entities.User; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CleanArc.Infrastructure.Persistence.Configuration.UserConfig; + +internal class UserTokenConfig:IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + + builder.HasOne(u => u.User).WithMany(u => u.Tokens).HasForeignKey(u => u.UserId); + builder.ToTable("UserTokens","usr"); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20210327210004_Init.Designer.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20210327210004_Init.Designer.cs new file mode 100644 index 0000000..601ff11 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20210327210004_Init.Designer.cs @@ -0,0 +1,374 @@ +// +using System; +using CleanArc.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Persistence; + +namespace Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20210327210004_Init")] + partial class Init + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .UseIdentityColumns() + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("ProductVersion", "5.0.0"); + + modelBuilder.Entity("Domain.Entities.User.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .UseIdentityColumn(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .UseIdentityColumn(); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedClaim") + .HasColumnType("datetime2"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("UserId") + .UseIdentityColumn(); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FamilyName") + .HasColumnType("nvarchar(max)"); + + b.Property("GeneratedCode") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("Users", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .UseIdentityColumn(); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("LoggedOn") + .HasColumnType("datetime2"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedTime") + .HasColumnType("datetime2"); + + b.Property("IsValid") + .HasColumnType("bit"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRefreshTokens", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.Property("CreatedUserRoleDate") + .HasColumnType("datetime2"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("GeneratedTime") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.RoleClaim", b => + { + b.HasOne("Domain.Entities.User.Role", "Role") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserClaim", b => + { + b.HasOne("Domain.Entities.User.User", "User") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserLogin", b => + { + b.HasOne("Domain.Entities.User.User", "User") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserRefreshToken", b => + { + b.HasOne("Domain.Entities.User.User", "User") + .WithMany("UserRefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserRole", b => + { + b.HasOne("Domain.Entities.User.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Domain.Entities.User.User", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserToken", b => + { + b.HasOne("Domain.Entities.User.User", "User") + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User.Role", b => + { + b.Navigation("Claims"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Domain.Entities.User.User", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("Tokens"); + + b.Navigation("UserRefreshTokens"); + + b.Navigation("UserRoles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20210327210004_Init.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20210327210004_Init.cs new file mode 100644 index 0000000..719b39a --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20210327210004_Init.cs @@ -0,0 +1,294 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Persistence.Migrations +{ + public partial class Init : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "usr"); + + migrationBuilder.CreateTable( + name: "Roles", + schema: "usr", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + DisplayName = table.Column(type: "nvarchar(max)", nullable: true), + CreatedDate = table.Column(type: "datetime2", nullable: false), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Roles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + schema: "usr", + columns: table => new + { + UserId = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: true), + FamilyName = table.Column(type: "nvarchar(max)", nullable: true), + GeneratedCode = table.Column(type: "nvarchar(max)", nullable: true), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), + SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), + TwoFactorEnabled = table.Column(type: "bit", nullable: false), + LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), + LockoutEnabled = table.Column(type: "bit", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.UserId); + }); + + migrationBuilder.CreateTable( + name: "RoleClaims", + schema: "usr", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + CreatedClaim = table.Column(type: "datetime2", nullable: false), + RoleId = table.Column(type: "int", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_RoleClaims_Roles_RoleId", + column: x => x.RoleId, + principalSchema: "usr", + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "UserClaims", + schema: "usr", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "int", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserClaims", x => x.Id); + table.ForeignKey( + name: "FK_UserClaims_Users_UserId", + column: x => x.UserId, + principalSchema: "usr", + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "UserLogins", + schema: "usr", + columns: table => new + { + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), + LoggedOn = table.Column(type: "datetime2", nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_UserLogins_Users_UserId", + column: x => x.UserId, + principalSchema: "usr", + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "UserRefreshTokens", + schema: "usr", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + UserId = table.Column(type: "int", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + IsValid = table.Column(type: "bit", nullable: false), + CreatedTime = table.Column(type: "datetime2", nullable: false), + ModifiedDate = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserRefreshTokens", x => x.Id); + table.ForeignKey( + name: "FK_UserRefreshTokens_Users_UserId", + column: x => x.UserId, + principalSchema: "usr", + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "UserRoles", + schema: "usr", + columns: table => new + { + UserId = table.Column(type: "int", nullable: false), + RoleId = table.Column(type: "int", nullable: false), + CreatedUserRoleDate = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_UserRoles_Roles_RoleId", + column: x => x.RoleId, + principalSchema: "usr", + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_UserRoles_Users_UserId", + column: x => x.UserId, + principalSchema: "usr", + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "UserTokens", + schema: "usr", + columns: table => new + { + UserId = table.Column(type: "int", nullable: false), + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + GeneratedTime = table.Column(type: "datetime2", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_UserTokens_Users_UserId", + column: x => x.UserId, + principalSchema: "usr", + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_RoleClaims_RoleId", + schema: "usr", + table: "RoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + schema: "usr", + table: "Roles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_UserClaims_UserId", + schema: "usr", + table: "UserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_UserLogins_UserId", + schema: "usr", + table: "UserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_UserRefreshTokens_UserId", + schema: "usr", + table: "UserRefreshTokens", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_UserRoles_RoleId", + schema: "usr", + table: "UserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + schema: "usr", + table: "Users", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + schema: "usr", + table: "Users", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RoleClaims", + schema: "usr"); + + migrationBuilder.DropTable( + name: "UserClaims", + schema: "usr"); + + migrationBuilder.DropTable( + name: "UserLogins", + schema: "usr"); + + migrationBuilder.DropTable( + name: "UserRefreshTokens", + schema: "usr"); + + migrationBuilder.DropTable( + name: "UserRoles", + schema: "usr"); + + migrationBuilder.DropTable( + name: "UserTokens", + schema: "usr"); + + migrationBuilder.DropTable( + name: "Roles", + schema: "usr"); + + migrationBuilder.DropTable( + name: "Users", + schema: "usr"); + } + } +} diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20221205084354_AddedOrderAndUserRelation.Designer.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20221205084354_AddedOrderAndUserRelation.Designer.cs new file mode 100644 index 0000000..641924b --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20221205084354_AddedOrderAndUserRelation.Designer.cs @@ -0,0 +1,422 @@ +// +using System; +using CleanArc.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Persistence; + +#nullable disable + +namespace Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20221205084354_AddedOrderAndUserRelation")] + partial class AddedOrderAndUserRelation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Order.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedTime") + .HasColumnType("datetime2"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("OrderName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Domain.Entities.User.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedClaim") + .HasColumnType("datetime2"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("UserId"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FamilyName") + .HasColumnType("nvarchar(max)"); + + b.Property("GeneratedCode") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("Users", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("LoggedOn") + .HasColumnType("datetime2"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedTime") + .HasColumnType("datetime2"); + + b.Property("IsValid") + .HasColumnType("bit"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRefreshTokens", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.Property("CreatedUserRoleDate") + .HasColumnType("datetime2"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("GeneratedTime") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", "usr"); + }); + + modelBuilder.Entity("Domain.Entities.Order.Order", b => + { + b.HasOne("Domain.Entities.User.User", "User") + .WithMany("Orders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User.RoleClaim", b => + { + b.HasOne("Domain.Entities.User.Role", "Role") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserClaim", b => + { + b.HasOne("Domain.Entities.User.User", "User") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserLogin", b => + { + b.HasOne("Domain.Entities.User.User", "User") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserRefreshToken", b => + { + b.HasOne("Domain.Entities.User.User", "User") + .WithMany("UserRefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserRole", b => + { + b.HasOne("Domain.Entities.User.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Domain.Entities.User.User", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User.UserToken", b => + { + b.HasOne("Domain.Entities.User.User", "User") + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User.Role", b => + { + b.Navigation("Claims"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Domain.Entities.User.User", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("Orders"); + + b.Navigation("Tokens"); + + b.Navigation("UserRefreshTokens"); + + b.Navigation("UserRoles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20221205084354_AddedOrderAndUserRelation.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20221205084354_AddedOrderAndUserRelation.cs new file mode 100644 index 0000000..87a1590 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20221205084354_AddedOrderAndUserRelation.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.Migrations +{ + /// + public partial class AddedOrderAndUserRelation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Orders", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + OrderName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "int", nullable: false), + CreatedTime = table.Column(type: "datetime2", nullable: false), + ModifiedDate = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + table.ForeignKey( + name: "FK_Orders_Users_UserId", + column: x => x.UserId, + principalSchema: "usr", + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Orders_UserId", + table: "Orders", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Orders"); + } + } +} diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20231126140035_AddedOrderDeleteFlag.Designer.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20231126140035_AddedOrderDeleteFlag.Designer.cs new file mode 100644 index 0000000..2b1a7f9 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20231126140035_AddedOrderDeleteFlag.Designer.cs @@ -0,0 +1,424 @@ +// +using System; +using CleanArc.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20231126140035_AddedOrderDeleteFlag")] + partial class AddedOrderDeleteFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CleanArc.Domain.Entities.Order.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("OrderName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedClaim") + .HasColumnType("datetime2"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("UserId"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FamilyName") + .HasColumnType("nvarchar(max)"); + + b.Property("GeneratedCode") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("Users", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("LoggedOn") + .HasColumnType("datetime2"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedTime") + .HasColumnType("datetime2"); + + b.Property("IsValid") + .HasColumnType("bit"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRefreshTokens", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.Property("CreatedUserRoleDate") + .HasColumnType("datetime2"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("GeneratedTime") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.Order.Order", b => + { + b.HasOne("CleanArc.Domain.Entities.User.User", "User") + .WithMany("Orders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.RoleClaim", b => + { + b.HasOne("CleanArc.Domain.Entities.User.Role", "Role") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserClaim", b => + { + b.HasOne("CleanArc.Domain.Entities.User.User", "User") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserLogin", b => + { + b.HasOne("CleanArc.Domain.Entities.User.User", "User") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserRefreshToken", b => + { + b.HasOne("CleanArc.Domain.Entities.User.User", "User") + .WithMany("UserRefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserRole", b => + { + b.HasOne("CleanArc.Domain.Entities.User.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("CleanArc.Domain.Entities.User.User", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserToken", b => + { + b.HasOne("CleanArc.Domain.Entities.User.User", "User") + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.Role", b => + { + b.Navigation("Claims"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.User", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("Orders"); + + b.Navigation("Tokens"); + + b.Navigation("UserRefreshTokens"); + + b.Navigation("UserRoles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20231126140035_AddedOrderDeleteFlag.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20231126140035_AddedOrderDeleteFlag.cs new file mode 100644 index 0000000..776b4a5 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/20231126140035_AddedOrderDeleteFlag.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.Migrations +{ + /// + public partial class AddedOrderDeleteFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "Orders", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "Orders"); + } + } +} diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..de30f7d --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,421 @@ +// +using System; +using CleanArc.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CleanArc.Domain.Entities.Order.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("OrderName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedClaim") + .HasColumnType("datetime2"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("UserId"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FamilyName") + .HasColumnType("nvarchar(max)"); + + b.Property("GeneratedCode") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("Users", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("LoggedOn") + .HasColumnType("datetime2"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedTime") + .HasColumnType("datetime2"); + + b.Property("IsValid") + .HasColumnType("bit"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRefreshTokens", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.Property("CreatedUserRoleDate") + .HasColumnType("datetime2"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("GeneratedTime") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", "usr"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.Order.Order", b => + { + b.HasOne("CleanArc.Domain.Entities.User.User", "User") + .WithMany("Orders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.RoleClaim", b => + { + b.HasOne("CleanArc.Domain.Entities.User.Role", "Role") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserClaim", b => + { + b.HasOne("CleanArc.Domain.Entities.User.User", "User") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserLogin", b => + { + b.HasOne("CleanArc.Domain.Entities.User.User", "User") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserRefreshToken", b => + { + b.HasOne("CleanArc.Domain.Entities.User.User", "User") + .WithMany("UserRefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserRole", b => + { + b.HasOne("CleanArc.Domain.Entities.User.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("CleanArc.Domain.Entities.User.User", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.UserToken", b => + { + b.HasOne("CleanArc.Domain.Entities.User.User", "User") + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.Role", b => + { + b.Navigation("Claims"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("CleanArc.Domain.Entities.User.User", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("Orders"); + + b.Navigation("Tokens"); + + b.Navigation("UserRefreshTokens"); + + b.Navigation("UserRoles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/Common/BaseAsyncRepository.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/Common/BaseAsyncRepository.cs new file mode 100644 index 0000000..169e3cb --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/Common/BaseAsyncRepository.cs @@ -0,0 +1,44 @@ +using System.Linq.Expressions; +using CleanArc.Domain.Common; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; + +namespace CleanArc.Infrastructure.Persistence.Repositories.Common; + +internal abstract class BaseAsyncRepository where TEntity:class,IEntity +{ + public readonly ApplicationDbContext DbContext; + protected DbSet Entities { get; } + protected virtual IQueryable Table => Entities; + protected virtual IQueryable TableNoTracking => Entities.AsNoTrackingWithIdentityResolution(); + + protected BaseAsyncRepository(ApplicationDbContext dbContext) + { + DbContext = dbContext; + Entities = DbContext.Set(); // City => Cities + } + + protected virtual async Task> ListAllAsync() + { + return await Entities.ToListAsync(); + } + + protected virtual async Task AddAsync(TEntity entity) + { + await Entities.AddAsync(entity); + + } + + + + protected virtual async Task UpdateAsync( + Expression> whereExpression,Action> updateExpression) + { + await Entities.Where(whereExpression).ExecuteUpdateAsync(updateExpression); + } + + protected virtual async Task DeleteAsync(Expression> deleteExpression) + { + await Entities.Where(deleteExpression).ExecuteDeleteAsync(); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/Common/UnitOfWork.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/Common/UnitOfWork.cs new file mode 100644 index 0000000..1d9c1e6 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/Common/UnitOfWork.cs @@ -0,0 +1,28 @@ +using CleanArc.Application.Contracts.Persistence; + +namespace CleanArc.Infrastructure.Persistence.Repositories.Common; + +public class UnitOfWork : IUnitOfWork +{ + private readonly ApplicationDbContext _db; + + public IUserRefreshTokenRepository UserRefreshTokenRepository { get; } + public IOrderRepository OrderRepository { get; } + + public UnitOfWork(ApplicationDbContext db) + { + _db = db; + UserRefreshTokenRepository = new UserRefreshTokenRepository(_db); + OrderRepository= new OrderRepository(_db); + } + + public Task CommitAsync() + { + return _db.SaveChangesAsync(); + } + + public ValueTask RollBackAsync() + { + return _db.DisposeAsync(); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/OrderRepository.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/OrderRepository.cs new file mode 100644 index 0000000..803a8cd --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/OrderRepository.cs @@ -0,0 +1,41 @@ +using CleanArc.Application.Contracts.Persistence; +using CleanArc.Domain.Entities.Order; +using CleanArc.Infrastructure.Persistence.Repositories.Common; +using Microsoft.EntityFrameworkCore; + +namespace CleanArc.Infrastructure.Persistence.Repositories; + +internal class OrderRepository(ApplicationDbContext dbContext) : BaseAsyncRepository(dbContext), IOrderRepository +{ + public async Task AddOrderAsync(Order order) + { + await base.AddAsync(order); + } + + public async Task> GetAllUserOrdersAsync(int userId) + { + return await base.TableNoTracking.Where(c => c.UserId == userId).ToListAsync(); + } + + public async Task> GetAllOrdersWithRelatedUserAsync() + { + var orders = await base.TableNoTracking.Include(c => c.User).ToListAsync(); + + return orders; + } + + public async Task GetUserOrderByIdAndUserIdAsync(int userId, int orderId,bool trackEntity) + { + var order = await base.TableNoTracking.FirstOrDefaultAsync(c => c.UserId == userId && c.Id == orderId); + + if (order is not null && trackEntity) + base.DbContext.Attach(order); + + return order; + } + + public async Task DeleteUserOrdersAsync(int userId) + { + await UpdateAsync(c => c.UserId == userId, p => p.SetProperty(order => order.IsDeleted, true)); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/UserRefreshTokenRepository.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/UserRefreshTokenRepository.cs new file mode 100644 index 0000000..2176e8f --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/Repositories/UserRefreshTokenRepository.cs @@ -0,0 +1,40 @@ +using CleanArc.Application.Contracts.Persistence; +using CleanArc.Domain.Entities.User; +using CleanArc.Infrastructure.Persistence.Repositories.Common; +using Microsoft.EntityFrameworkCore; + +namespace CleanArc.Infrastructure.Persistence.Repositories; + +internal class UserRefreshTokenRepository : BaseAsyncRepository, IUserRefreshTokenRepository +{ + public UserRefreshTokenRepository(ApplicationDbContext dbContext) : base(dbContext) + { + } + + public async Task CreateToken(int userId) + { + var token = new UserRefreshToken { IsValid = true, UserId = userId }; + await base.AddAsync(token); + return token.Id; + } + + public async Task GetTokenWithInvalidation(Guid id) + { + var token = await base.Table.Where(t => t.IsValid && t.Id.Equals(id)).FirstOrDefaultAsync(); + + return token; + } + + public async Task GetUserByRefreshToken(Guid tokenId) + { + var user = await base.TableNoTracking.Include(t => t.User).Where(c => c.Id.Equals(tokenId)) + .Select(c => c.User).FirstOrDefaultAsync(); + + return user; + } + + public Task RemoveUserOldTokens(int userId, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..70d0de4 --- /dev/null +++ b/server/src/Infrastructure/CleanArc.Infrastructure.Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs @@ -0,0 +1,35 @@ +using CleanArc.Application.Contracts.Persistence; +using CleanArc.Infrastructure.Persistence.Repositories.Common; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace CleanArc.Infrastructure.Persistence.ServiceConfiguration; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddPersistenceServices(this IServiceCollection services,IConfiguration configuration) + { + services.AddScoped(); + + services.AddDbContext(options => + { + options + .UseSqlServer(configuration.GetConnectionString("SqlServer")); + }); + + return services; + } + + public static async Task ApplyMigrationsAsync(this WebApplication app) + { + await using var scope = app.Services.CreateAsyncScope(); + var context = scope.ServiceProvider.GetService(); + + if (context is null) + throw new Exception("Database Context Not Found"); + + await context.Database.MigrateAsync(); + } +} \ No newline at end of file diff --git a/server/src/Shared/CleanArc.SharedKernel/CleanArc.SharedKernel.csproj b/server/src/Shared/CleanArc.SharedKernel/CleanArc.SharedKernel.csproj new file mode 100644 index 0000000..80595cd --- /dev/null +++ b/server/src/Shared/CleanArc.SharedKernel/CleanArc.SharedKernel.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + enable + + + + + + + + + + + + + + diff --git a/server/src/Shared/CleanArc.SharedKernel/Extensions/AssemblyExtensions.cs b/server/src/Shared/CleanArc.SharedKernel/Extensions/AssemblyExtensions.cs new file mode 100644 index 0000000..526e83f --- /dev/null +++ b/server/src/Shared/CleanArc.SharedKernel/Extensions/AssemblyExtensions.cs @@ -0,0 +1,19 @@ +using System.Reflection; + +namespace CleanArc.SharedKernel.Extensions; + +public static class AssemblyExtensions +{ + public static IEnumerable GetDependentAssemblies(this Assembly analyzedAssembly) + { + return AppDomain.CurrentDomain.GetAssemblies() + .Where(a => GetNamesOfAssembliesReferencedBy(a) + .Contains(analyzedAssembly.FullName)); + } + + public static IEnumerable GetNamesOfAssembliesReferencedBy(Assembly assembly) + { + return assembly.GetReferencedAssemblies() + .Select(assemblyName => assemblyName.FullName); + } +} \ No newline at end of file diff --git a/server/src/Shared/CleanArc.SharedKernel/Extensions/CollectionExtensions.cs b/server/src/Shared/CleanArc.SharedKernel/Extensions/CollectionExtensions.cs new file mode 100644 index 0000000..57aa905 --- /dev/null +++ b/server/src/Shared/CleanArc.SharedKernel/Extensions/CollectionExtensions.cs @@ -0,0 +1,15 @@ +namespace CleanArc.SharedKernel.Extensions; + +public static class CollectionExtensions +{ + public static Dictionary> ToGroupedDictionary( + this IEnumerable> keyValuePairs) + { + return keyValuePairs + .GroupBy(kvp => kvp.Key) + .ToDictionary( + group => group.Key, + group => group.Select(kvp => kvp.Value).ToList() + ); + } +} \ No newline at end of file diff --git a/server/src/Shared/CleanArc.SharedKernel/Extensions/EnumExtentions.cs b/server/src/Shared/CleanArc.SharedKernel/Extensions/EnumExtentions.cs new file mode 100644 index 0000000..c26a4a7 --- /dev/null +++ b/server/src/Shared/CleanArc.SharedKernel/Extensions/EnumExtentions.cs @@ -0,0 +1,54 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; + +namespace CleanArc.SharedKernel.Extensions; + +public static class EnumExtensions +{ + public static IEnumerable GetEnumValues(this T input) where T : struct + { + if (!typeof(T).IsEnum) + throw new NotSupportedException(); + + return Enum.GetValues(input.GetType()).Cast(); + } + + public static IEnumerable GetEnumFlags(this T input) where T : struct + { + if (!typeof(T).IsEnum) + throw new NotSupportedException(); + + foreach (var value in Enum.GetValues(input.GetType())) + if ((input as Enum).HasFlag(value as Enum)) + yield return (T)value; + } + + public static string ToDisplay(this Enum value, DisplayProperty property = DisplayProperty.Name) + { + ArgumentNullException.ThrowIfNull(value, nameof(value)); + + var attribute = value.GetType().GetField(value.ToString())! + .GetCustomAttributes(false).FirstOrDefault(); + + if (attribute == null) + return value.ToString(); + + var propValue = attribute.GetType().GetProperty(property.ToString()).GetValue(attribute, null); + return propValue.ToString(); + } + + public static Dictionary ToDictionary(this Enum value) + { + return Enum.GetValues(value.GetType()).Cast().ToDictionary(p => Convert.ToInt32(p), q => q.ToDisplay()); + } +} + +public enum DisplayProperty +{ + Description, + GroupName, + Name, + Prompt, + ShortName, + Order +} \ No newline at end of file diff --git a/server/src/Shared/CleanArc.SharedKernel/Extensions/IdentityExtensions.cs b/server/src/Shared/CleanArc.SharedKernel/Extensions/IdentityExtensions.cs new file mode 100644 index 0000000..2496217 --- /dev/null +++ b/server/src/Shared/CleanArc.SharedKernel/Extensions/IdentityExtensions.cs @@ -0,0 +1,43 @@ +using System.Globalization; +using System.Security.Claims; +using System.Security.Principal; +using Microsoft.AspNetCore.Identity; + +namespace CleanArc.SharedKernel.Extensions; + +public static class IdentityExtensions +{ + public static string FindFirstValue(this ClaimsIdentity identity, string claimType) + { + return identity?.FindFirst(claimType)?.Value; + } + + public static string FindFirstValue(this IIdentity identity, string claimType) + { + var claimsIdentity = identity as ClaimsIdentity; + return claimsIdentity?.FindFirstValue(claimType); + } + + public static string GetUserId(this IIdentity identity) + { + return identity?.FindFirstValue(ClaimTypes.NameIdentifier); + } + + public static T GetUserId(this IIdentity identity) where T : IConvertible + { + var userId = identity?.GetUserId(); + return userId.HasValue() + ? (T)Convert.ChangeType(userId, typeof(T), CultureInfo.InvariantCulture) + : default; + } + + public static string GetUserName(this IIdentity identity) + { + return identity?.FindFirstValue(ClaimTypes.Name); + } + + public static string StringifyIdentityResultErrors(this IEnumerable identity) + { + return string.Join("\n", identity.Select(c => c.Description)); + } +} \ No newline at end of file diff --git a/server/src/Shared/CleanArc.SharedKernel/Extensions/ModelBuilderExtensions.cs b/server/src/Shared/CleanArc.SharedKernel/Extensions/ModelBuilderExtensions.cs new file mode 100644 index 0000000..9f3d62f --- /dev/null +++ b/server/src/Shared/CleanArc.SharedKernel/Extensions/ModelBuilderExtensions.cs @@ -0,0 +1,120 @@ +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Pluralize.NET; + +namespace CleanArc.SharedKernel.Extensions; + +public static class ModelBuilderExtensions +{ + /// + /// Singularizin table name like Posts to Post or People to Person + /// + /// + public static void AddSingularizingTableNameConvention(this ModelBuilder modelBuilder) + { + Pluralizer pluralizer = new Pluralizer(); + foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) + { + string tableName = entityType.GetTableName(); + + if (!string.IsNullOrEmpty(tableName)) + entityType.SetTableName(pluralizer.Pluralize(tableName)); + } + } + + /// + /// Pluralizing table name like Post to Posts or Person to People + /// + /// + public static void AddPluralizingTableNameConvention(this ModelBuilder modelBuilder) + { + Pluralizer pluralizer = new Pluralizer(); + foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) + { + string tableName = entityType.GetTableName(); + entityType.SetTableName(pluralizer.Pluralize(tableName)); + } + } + + /// + /// Set NEWSEQUENTIALID() sql function for all columns named "Id" + /// + /// + /// Set to true if you want only "Identity" guid fields that named "Id" + public static void AddSequentialGuidForIdConvention(this ModelBuilder modelBuilder) + { + modelBuilder.AddDefaultValueSqlConvention("Id", typeof(Guid), "NEWSEQUENTIALID()"); + } + + /// + /// Set DefaultValueSql for sepecific property name and type + /// + /// + /// Name of property wants to set DefaultValueSql for + /// Type of property wants to set DefaultValueSql for + /// DefaultValueSql like "NEWSEQUENTIALID()" + public static void AddDefaultValueSqlConvention(this ModelBuilder modelBuilder, string propertyName, Type propertyType, string defaultValueSql) + { + foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) + { + IMutableProperty property = entityType.GetProperties().SingleOrDefault(p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); + if (property != null && property.ClrType == propertyType) + property.SetDefaultValueSql(defaultValueSql); + } + } + + /// + /// Set DeleteBehavior.Restrict by default for relations + /// + /// + public static void AddRestrictDeleteBehaviorConvention(this ModelBuilder modelBuilder) + { + IEnumerable cascadeFKs = modelBuilder.Model.GetEntityTypes() + .SelectMany(t => t.GetForeignKeys()) + .Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade); + foreach (IMutableForeignKey fk in cascadeFKs) + fk.DeleteBehavior = DeleteBehavior.Restrict; + } + + /// + /// Dynamicaly load all IEntityTypeConfiguration with Reflection + /// + /// + /// Assemblies contains Entities + [Obsolete("Use the ApplyConfigurationsFromAssembly instead")] + public static void RegisterEntityTypeConfiguration(this ModelBuilder modelBuilder, params Assembly[] assemblies) + { + MethodInfo applyGenericMethod = typeof(ModelBuilder).GetMethods().First(m => m.Name == nameof(ModelBuilder.ApplyConfiguration)); + + IEnumerable types = assemblies.SelectMany(a => a.GetExportedTypes()) + .Where(c => c.IsClass && !c.IsAbstract && c.IsPublic); + + foreach (Type type in types) + { + foreach (Type iface in type.GetInterfaces()) + { + if (iface.IsConstructedGenericType && iface.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>)) + { + MethodInfo applyConcreteMethod = applyGenericMethod.MakeGenericMethod(iface.GenericTypeArguments[0]); + applyConcreteMethod.Invoke(modelBuilder, new object[] { Activator.CreateInstance(type) }); + } + } + } + } + + /// + /// Dynamicaly register all Entities that inherit from specific BaseType + /// + /// + /// Base type that Entities inherit from this + /// Assemblies contains Entities + public static void RegisterAllEntities(this ModelBuilder modelBuilder, params Assembly[] assemblies) + { + IEnumerable types = assemblies.SelectMany(a => a.GetExportedTypes()) + .Where(c => c.IsClass && !c.IsAbstract && c.IsPublic && typeof(BaseType).IsAssignableFrom(c)); + + foreach (Type type in types) + modelBuilder.Entity(type); + } +} \ No newline at end of file diff --git a/server/src/Shared/CleanArc.SharedKernel/Extensions/ReflectionExtensions.cs b/server/src/Shared/CleanArc.SharedKernel/Extensions/ReflectionExtensions.cs new file mode 100644 index 0000000..df08ee0 --- /dev/null +++ b/server/src/Shared/CleanArc.SharedKernel/Extensions/ReflectionExtensions.cs @@ -0,0 +1,102 @@ +using System.Collections; +using System.Reflection; + +namespace CleanArc.SharedKernel.Extensions; + +public static class ReflectionHelper +{ + public static bool HasAttribute(this MemberInfo type, bool inherit = false) where T : Attribute + { + return type.HasAttribute(typeof(T), inherit); + } + + public static bool HasAttribute(this MemberInfo type, Type attribute, bool inherit = false) + { + return Attribute.IsDefined(type, attribute, inherit); + //return type.IsDefined(attribute, inherit); + //return type.GetCustomAttributes(attribute, inherit).Length > 0; + } + + public static bool IsInheritFrom(this Type type) + { + return type.IsInheritFrom(typeof(T)); + } + + public static bool IsInheritFrom(this Type type, Type parentType) + { + //the 'is' keyword do this too for values (new ChildClass() is ParentClass) + return parentType.IsAssignableFrom(type); + } + + public static bool BaseTypeIsGeneric(this Type type, Type genericType) + { + return type.BaseType?.IsGenericType == true && type.BaseType.GetGenericTypeDefinition() == genericType; + } + + public static IEnumerable GetTypesAssignableFrom(params Assembly[] assemblies) + { + return typeof(T).GetTypesAssignableFrom(assemblies); + } + + public static IEnumerable GetTypesAssignableFrom(this Type type, params Assembly[] assemblies) + { + return assemblies.SelectMany(p => p.GetTypes()).Where(p => p.IsInheritFrom(type)); + } + + public static IEnumerable GetTypesHasAttribute(params Assembly[] assemblies) where T : Attribute + { + return typeof(T).GetTypesHasAttribute(assemblies); + } + + public static IEnumerable GetTypesHasAttribute(this Type type, params Assembly[] assemblies) + { + return assemblies.SelectMany(p => p.GetTypes()).Where(p => p.HasAttribute(type)); + } + + public static bool IsEnumerable(this Type type) + { + return type != typeof(string) && type.IsInheritFrom(); + } + + public static bool IsEnumerable(this Type type) + { + return type != typeof(string) && type.IsInheritFrom>() && type.IsGenericType; + } + + public static IEnumerable GetBaseTypesAndInterfaces(this Type type) + { + if (type == null || type.BaseType == null) + yield break; + + foreach (var i in type.GetInterfaces()) + yield return i; + + var currentBaseType = type.BaseType; + while (currentBaseType != null) + { + yield return currentBaseType; + currentBaseType = currentBaseType.BaseType; + } + } + + public static bool IsCustomType(this Type type) + { + //return type.Assembly.GetName().Name != "mscorlib"; + return type.IsCustomValueType() || type.IsCustomReferenceType(); + } + + public static bool IsCustomValueType(this Type type) + { + return type.IsValueType && !type.IsPrimitive && type.Namespace != null && !type.Namespace.StartsWith("System", StringComparison.Ordinal); + } + + public static bool IsCustomReferenceType(this Type type) + { + return !type.IsValueType && !type.IsPrimitive && type.Namespace != null && !type.Namespace.StartsWith("System", StringComparison.Ordinal); + } + + public static bool IsAssignableFromBaseTypeGeneric(this object obj, Type type) + { + return obj.GetType().GetGenericTypeDefinition().IsAssignableFrom(type); + } +} \ No newline at end of file diff --git a/server/src/Shared/CleanArc.SharedKernel/Extensions/RegExHelpers.cs b/server/src/Shared/CleanArc.SharedKernel/Extensions/RegExHelpers.cs new file mode 100644 index 0000000..3d10193 --- /dev/null +++ b/server/src/Shared/CleanArc.SharedKernel/Extensions/RegExHelpers.cs @@ -0,0 +1,15 @@ +using System.Text.RegularExpressions; + +namespace CleanArc.SharedKernel.Extensions; + +public static class RegExHelpers +{ + public static bool MatchesApiVersion(string apiVersion, string text) + { + string pattern = $@"(?<=\/|^){Regex.Escape(apiVersion)}(?=\/|$)"; + + Regex exactMatchRegex = new Regex(pattern, RegexOptions.Compiled); + + return exactMatchRegex.IsMatch(text); + } +} \ No newline at end of file diff --git a/server/src/Shared/CleanArc.SharedKernel/Extensions/StringExtensions.cs b/server/src/Shared/CleanArc.SharedKernel/Extensions/StringExtensions.cs new file mode 100644 index 0000000..97322f6 --- /dev/null +++ b/server/src/Shared/CleanArc.SharedKernel/Extensions/StringExtensions.cs @@ -0,0 +1,103 @@ +namespace CleanArc.SharedKernel.Extensions; + +public static class StringExtensions +{ + public static bool HasValue(this string value, bool ignoreWhiteSpace = true) + { + return ignoreWhiteSpace ? !string.IsNullOrWhiteSpace(value) : !string.IsNullOrEmpty(value); + } + + public static int ToInt(this string value) + { + return Convert.ToInt32(value); + } + + public static decimal ToDecimal(this string value) + { + return Convert.ToDecimal(value); + } + + public static string ToNumeric(this int value) + { + return value.ToString("N0"); //"123,456" + } + + public static string ToNumeric(this decimal value) + { + return value.ToString("N0"); + } + + public static string ToCurrency(this int value) + { + //fa-IR => current culture currency symbol => ریال + //123456 => "123,123ریال" + return value.ToString("C0"); + } + + public static string ToCurrency(this decimal value) + { + return value.ToString("C0"); + } + + public static string En2Fa(this string str) + { + return str.Replace("0", "۰") + .Replace("1", "۱") + .Replace("2", "۲") + .Replace("3", "۳") + .Replace("4", "۴") + .Replace("5", "۵") + .Replace("6", "۶") + .Replace("7", "۷") + .Replace("8", "۸") + .Replace("9", "۹"); + } + + public static string Fa2En(this string str) + { + return str.Replace("۰", "0") + .Replace("۱", "1") + .Replace("۲", "2") + .Replace("۳", "3") + .Replace("۴", "4") + .Replace("۵", "5") + .Replace("۶", "6") + .Replace("۷", "7") + .Replace("۸", "8") + .Replace("۹", "9") + //iphone numeric + .Replace("٠", "0") + .Replace("١", "1") + .Replace("٢", "2") + .Replace("٣", "3") + .Replace("٤", "4") + .Replace("٥", "5") + .Replace("٦", "6") + .Replace("٧", "7") + .Replace("٨", "8") + .Replace("٩", "9"); + } + + public static string FixPersianChars(this string str) + { + return str.Replace("ﮎ", "ک") + .Replace("ﮏ", "ک") + .Replace("ﮐ", "ک") + .Replace("ﮑ", "ک") + .Replace("ك", "ک") + .Replace("ي", "ی") + .Replace(" ", " ") + .Replace("‌", " ") + .Replace("ھ", "ه");//.Replace("ئ", "ی"); + } + + public static string CleanString(this string str) + { + return str.Trim().FixPersianChars().Fa2En().NullIfEmpty(); + } + + public static string NullIfEmpty(this string str) + { + return str?.Length == 0 ? null : str; + } +} \ No newline at end of file diff --git a/server/src/Shared/CleanArc.SharedKernel/Extensions/ValidatorExtensions.cs b/server/src/Shared/CleanArc.SharedKernel/Extensions/ValidatorExtensions.cs new file mode 100644 index 0000000..ed5bcab --- /dev/null +++ b/server/src/Shared/CleanArc.SharedKernel/Extensions/ValidatorExtensions.cs @@ -0,0 +1,50 @@ +using CleanArc.SharedKernel.ValidationBase; +using CleanArc.SharedKernel.ValidationBase.Contracts; +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; + +namespace CleanArc.SharedKernel.Extensions +{ + public static class ValidatorExtensions + { + public static IServiceCollection RegisterValidatorsAsServices(this IServiceCollection services) + { + + var types = AppDomain.CurrentDomain.GetAssemblies().Where(c => c != typeof(ValidatorExtensions).Assembly).SelectMany(a => a.GetExportedTypes()).Where(t => t.GetInterfaces().Any(i => + i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IValidatableModel<>))) + .ToList(); + + + + foreach (var type in types) + { + var typeConstructorArgumentLength = type.GetConstructors().OrderByDescending(c=>c.GetParameters().Length).First().GetParameters().Length; + var model = Activator.CreateInstance(type, new object[typeConstructorArgumentLength]); + + var methodInfo = type.GetMethod(nameof(IValidatableModel.ValidateApplicationModel)); + + + if (model != null) + { + var methodArgument = Activator.CreateInstance(typeof(ApplicationBaseValidationModelProvider<>).MakeGenericType(type)); + var validator = methodInfo?.Invoke(model, new[] { methodArgument }); + + if (validator != null) + { + var interfaces = validator.GetType().GetInterfaces(); + + + var validatorInterface = interfaces + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IValidator<>)); + + if (validatorInterface != null) + services.AddScoped(validatorInterface, _ => validator); + + } + } + } + return services; + } + } + +} diff --git a/server/src/Shared/CleanArc.SharedKernel/ValidationBase/ApplicationBaseValidationModelProvider.cs b/server/src/Shared/CleanArc.SharedKernel/ValidationBase/ApplicationBaseValidationModelProvider.cs new file mode 100644 index 0000000..8f1ea6a --- /dev/null +++ b/server/src/Shared/CleanArc.SharedKernel/ValidationBase/ApplicationBaseValidationModelProvider.cs @@ -0,0 +1,6 @@ +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; + +namespace CleanArc.SharedKernel.ValidationBase; + +public class ApplicationBaseValidationModelProvider:AbstractValidator; \ No newline at end of file diff --git a/server/src/Shared/CleanArc.SharedKernel/ValidationBase/Contracts/IValidatableModel.cs b/server/src/Shared/CleanArc.SharedKernel/ValidationBase/Contracts/IValidatableModel.cs new file mode 100644 index 0000000..153f993 --- /dev/null +++ b/server/src/Shared/CleanArc.SharedKernel/ValidationBase/Contracts/IValidatableModel.cs @@ -0,0 +1,8 @@ +using FluentValidation; + +namespace CleanArc.SharedKernel.ValidationBase.Contracts; + +public interface IValidatableModel where TApplicationModel:class +{ + IValidator ValidateApplicationModel(ApplicationBaseValidationModelProvider validator); +} \ No newline at end of file diff --git a/server/src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity.csproj b/server/src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity.csproj new file mode 100644 index 0000000..7651606 --- /dev/null +++ b/server/src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/server/src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/UserManagerTest.cs b/server/src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/UserManagerTest.cs new file mode 100644 index 0000000..546c2be --- /dev/null +++ b/server/src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/UserManagerTest.cs @@ -0,0 +1,96 @@ +using CleanArc.Domain.Entities.User; +using CleanArc.Tests.Setup.Setups; + +namespace CleanArc.Test.Infrastructure.Identity +{ + public class UserManagerTest: TestIdentitySetup + { + [Fact] + public async Task Duplicate_User_Names_Not_Allowed() + { + var user = new User() + { + UserName = "Test", Email = "Test@example.com" + }; + + var duplicateUser = new User() + { + UserName = "Test", + Email = "Test@example.com" + }; + + var createUserResult = await base.TestAppUserManager.CreateUser(user); + + var duplicateCreateUserResult = await base.TestAppUserManager.CreateUser(duplicateUser); + + Assert.False(duplicateCreateUserResult.Succeeded); + } + + [Fact] + public async Task Create_Phone_Confirmation_Code_And_Verify() + { + var user = new User() + { + UserName = "Test", + Email = "test@example.com", + PhoneNumber = "09123456789" + }; + + var createUserResult = await base.TestAppUserManager.CreateUser(user); + + var otpCode=await base.TestAppUserManager.GeneratePhoneNumberConfirmationToken(user,user.PhoneNumber); + + var confirmPhoneNumberResult=await base.TestAppUserManager.ChangePhoneNumber(user,user.PhoneNumber,otpCode); + + + Assert.NotNull(otpCode); + Assert.True(confirmPhoneNumberResult.Succeeded); + Assert.True(user.PhoneNumberConfirmed); + } + + [Fact] + public async Task Generate_And_Verify_Otp_Code() + { + var user = new User() + { + UserName = "Test", + Email = "test@example.com", + PhoneNumber = "09123456789" + }; + + var createUserResult = await base.TestAppUserManager.CreateUser(user); + + var phoneNumberConfirmationCode = await base.TestAppUserManager.GeneratePhoneNumberConfirmationToken(user, user.PhoneNumber); + + var confirmPhoneNumberResult = await base.TestAppUserManager.ChangePhoneNumber(user, user.PhoneNumber, phoneNumberConfirmationCode); + + var otpCode = await base.TestAppUserManager.GenerateOtpCode(user); + + var confirmOtpCode = await base.TestAppUserManager.VerifyUserCode(user, otpCode); + + Assert.NotNull(otpCode); + Assert.True(confirmOtpCode.Succeeded); + } + + [Fact] + public async Task Generate_Access_Token() + { + var user = new User() + { + UserName = "Test", + Email = "test@example.com", + PhoneNumber = "09123456789" + }; + + var createUserResult = await base.TestAppUserManager.CreateUser(user); + + var otpCode = await base.TestAppUserManager.GeneratePhoneNumberConfirmationToken(user, user.PhoneNumber); + + var confirmPhoneNumberResult = await base.TestAppUserManager.ChangePhoneNumber(user, user.PhoneNumber, otpCode); + + var token=await base.JwtService.GenerateAsync(user); + + Assert.NotNull(token.access_token); + } + } +} \ No newline at end of file diff --git a/server/src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/Usings.cs b/server/src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/server/src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/server/src/Tests/CleanArc.Tests.Setup/CleanArc.Tests.Setup.csproj b/server/src/Tests/CleanArc.Tests.Setup/CleanArc.Tests.Setup.csproj new file mode 100644 index 0000000..1ef867f --- /dev/null +++ b/server/src/Tests/CleanArc.Tests.Setup/CleanArc.Tests.Setup.csproj @@ -0,0 +1,34 @@ + + + + net10.0 + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/server/src/Tests/CleanArc.Tests.Setup/Setups/TestApplicationDbContext.cs b/server/src/Tests/CleanArc.Tests.Setup/Setups/TestApplicationDbContext.cs new file mode 100644 index 0000000..418a4fe --- /dev/null +++ b/server/src/Tests/CleanArc.Tests.Setup/Setups/TestApplicationDbContext.cs @@ -0,0 +1,23 @@ +using CleanArc.Infrastructure.Persistence; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace CleanArc.Tests.Setup.Setups; + +public abstract class TestApplicationDbContext +{ + protected ApplicationDbContext UnitTestDbContext { get; } + + protected TestApplicationDbContext() + { + var connectionStringBuilder = + new SqliteConnectionStringBuilder { DataSource = ":memory:" }; + var connection = new SqliteConnection(connectionStringBuilder.ToString()); + + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + + UnitTestDbContext = new ApplicationDbContext(options); + } +} \ No newline at end of file diff --git a/server/src/Tests/CleanArc.Tests.Setup/Setups/TestIdentitySetup.cs b/server/src/Tests/CleanArc.Tests.Setup/Setups/TestIdentitySetup.cs new file mode 100644 index 0000000..87e94dc --- /dev/null +++ b/server/src/Tests/CleanArc.Tests.Setup/Setups/TestIdentitySetup.cs @@ -0,0 +1,88 @@ +using CleanArc.Application.Contracts; +using CleanArc.Application.Contracts.Identity; +using CleanArc.Application.Contracts.Persistence; +using CleanArc.Domain.Entities.User; +using CleanArc.Infrastructure.Identity.Identity.Dtos; +using CleanArc.Infrastructure.Identity.Identity.Extensions; +using CleanArc.Infrastructure.Identity.Identity.Manager; +using CleanArc.Infrastructure.Identity.Identity.Store; +using CleanArc.Infrastructure.Identity.Jwt; +using CleanArc.Infrastructure.Identity.UserManager; +using CleanArc.Infrastructure.Persistence; +using CleanArc.Infrastructure.Persistence.Repositories.Common; +using Microsoft.AspNetCore.Identity; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace CleanArc.Tests.Setup.Setups; + +public abstract class TestIdentitySetup +{ + protected IAppUserManager TestAppUserManager { get; } + protected AppRoleManager TestAppRoleManager { get; } + public AppSignInManager TestSignInManager { get; } + public IAppUserManager User { get; } + public IJwtService JwtService { get; } + + protected TestIdentitySetup() + { + var serviceCollection = new ServiceCollection(); + + var connection = new SqliteConnection("DataSource=:memory:"); + + serviceCollection.AddLogging(); + + serviceCollection.AddDbContext(options => options.UseSqlite(connection)); + + var context = serviceCollection.BuildServiceProvider().GetService(); + context.Database.OpenConnection(); + context.Database.EnsureCreated(); + + + serviceCollection.AddIdentity(options => + { + options.Stores.ProtectPersonalData = false; + + options.Password.RequireDigit = false; + options.Password.RequireLowercase = false; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequiredUniqueChars = 0; + options.Password.RequireUppercase = false; + + options.SignIn.RequireConfirmedEmail = false; + options.SignIn.RequireConfirmedPhoneNumber = true; + + options.Lockout.MaxFailedAccessAttempts = 5; + options.Lockout.AllowedForNewUsers = false; + options.User.RequireUniqueEmail = false; + + }).AddUserStore() + .AddRoleStore(). + AddUserManager(). + AddRoleManager(). + AddDefaultTokenProviders(). + AddSignInManager() + .AddDefaultTokenProviders() + .AddPasswordlessLoginTotpTokenProvider(); + + serviceCollection.Configure(settings => + { + settings.Audience = "CleanArc.Unit.Tests"; + settings.ExpirationMinutes = 5; + settings.Issuer = "CleanArc.Unit.Tests"; + settings.NotBeforeMinutes = 0; + settings.SecretKey = "ShouldBe-LongerThan-16Char-SecretKey"; + settings.Encryptkey = "16CharEncryptKey"; + }); + + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + var serviceProvider = serviceCollection.BuildServiceProvider(); + TestAppUserManager = serviceProvider.GetRequiredService(); + TestAppRoleManager = serviceProvider.GetRequiredService(); + TestSignInManager = serviceProvider.GetRequiredService(); + JwtService = serviceProvider.GetRequiredService(); + } +} diff --git a/server/src/Tests/CleanArc.Tests.Setup/Usings.cs b/server/src/Tests/CleanArc.Tests.Setup/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/server/src/Tests/CleanArc.Tests.Setup/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file