From 0b7d31a20cef7b927cb8741063986ba2b66d5487 Mon Sep 17 00:00:00 2001 From: Mir Arif Hasan Date: Mon, 28 Jul 2025 17:16:30 +0600 Subject: [PATCH] feature: reduce `.env` usage and move configurations to admin dashboard (#5194) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: nivedin Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com> --- .env.example | 55 -- packages/hoppscotch-backend/package.json | 1 + packages/hoppscotch-backend/src/app.module.ts | 6 +- .../src/auth/auth.controller.ts | 7 +- .../src/auth/auth.module.ts | 9 +- .../src/auth/auth.service.ts | 10 +- .../hoppscotch-backend/src/auth/helper.ts | 26 +- .../src/auth/strategies/jwt.strategy.ts | 2 +- .../src/auth/strategies/rt-jwt.strategy.ts | 2 +- packages/hoppscotch-backend/src/errors.ts | 25 - .../src/infra-config/dto/onboarding.dto.ts | 200 ++++ .../src/infra-config/helper.ts | 403 ++++---- .../src/infra-config/infra-config.module.ts | 5 +- .../infra-config/infra-config.service.spec.ts | 3 + .../src/infra-config/infra-config.service.ts | 281 ++++-- .../src/infra-config/onboarding.controller.ts | 96 ++ .../hoppscotch-backend/src/mailer/helper.ts | 36 +- .../src/mailer/mailer.module.ts | 6 +- packages/hoppscotch-backend/src/main.ts | 21 +- .../src/team/team.service.spec.ts | 4 +- .../src/types/InfraConfig.ts | 14 + .../user-settings.service.spec.ts | 4 +- packages/hoppscotch-backend/src/utils.ts | 43 +- packages/hoppscotch-common/locales/en.json | 2 +- .../src/components/firebase/Login.vue | 22 + packages/hoppscotch-sh-admin/locales/en.json | 21 + .../src/components/app/Login.vue | 28 +- .../src/components/onboarding/AuthSetup.vue | 304 ++++++ .../components/onboarding/CompleteScreen.vue | 112 +++ .../src/components/onboarding/OAuthSetup.vue | 102 ++ .../src/components/onboarding/SmtpSetup.vue | 160 +++ .../components/onboarding/WelcomeScreen.vue | 18 + .../settings/AuthConfigurations.vue | 8 +- .../src/components/settings/AuthToken.vue | 205 ++++ .../settings/OAuthProviderConfigurations.vue | 27 +- .../src/components/settings/RateLimit.vue | 111 +++ .../src/components/settings/ServerRestart.vue | 18 + .../components/settings/SmtpConfiguration.vue | 23 +- .../src/components/ui/Accordion.vue | 43 + .../src/composables/useConfigHandler.ts | 168 +++- .../composables/useOnboardingConfigHandler.ts | 376 +++++++ .../hoppscotch-sh-admin/src/helpers/auth.ts | 37 + .../src/helpers/backend/rest/authQuery.ts | 5 + .../src/helpers/configs.ts | 62 +- .../src/helpers/utils/readableKey.ts | 20 + .../hoppscotch-sh-admin/src/modules/admin.ts | 25 +- .../src/pages/onboarding.vue | 135 +++ .../src/pages/settings.vue | 5 +- pnpm-lock.yaml | 933 ++++++++++-------- 49 files changed, 3330 insertions(+), 899 deletions(-) create mode 100644 packages/hoppscotch-backend/src/infra-config/dto/onboarding.dto.ts create mode 100644 packages/hoppscotch-backend/src/infra-config/onboarding.controller.ts create mode 100644 packages/hoppscotch-sh-admin/src/components/onboarding/AuthSetup.vue create mode 100644 packages/hoppscotch-sh-admin/src/components/onboarding/CompleteScreen.vue create mode 100644 packages/hoppscotch-sh-admin/src/components/onboarding/OAuthSetup.vue create mode 100644 packages/hoppscotch-sh-admin/src/components/onboarding/SmtpSetup.vue create mode 100644 packages/hoppscotch-sh-admin/src/components/onboarding/WelcomeScreen.vue create mode 100644 packages/hoppscotch-sh-admin/src/components/settings/AuthToken.vue create mode 100644 packages/hoppscotch-sh-admin/src/components/settings/RateLimit.vue create mode 100644 packages/hoppscotch-sh-admin/src/components/ui/Accordion.vue create mode 100644 packages/hoppscotch-sh-admin/src/composables/useOnboardingConfigHandler.ts create mode 100644 packages/hoppscotch-sh-admin/src/helpers/utils/readableKey.ts create mode 100644 packages/hoppscotch-sh-admin/src/pages/onboarding.vue diff --git a/.env.example b/.env.example index bb1f0ee2..c1e537b4 100644 --- a/.env.example +++ b/.env.example @@ -2,24 +2,9 @@ # Prisma Config DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch -# Auth Tokens Config -JWT_SECRET="secret1233" -TOKEN_SALT_COMPLEXITY=10 -MAGIC_LINK_TOKEN_VALIDITY= 3 -# Default validity is 7 days (604800000 ms) in ms -REFRESH_TOKEN_VALIDITY="604800000" -# Default validity is 1 day (86400000 ms) in ms -ACCESS_TOKEN_VALIDITY="86400000" -SESSION_SECRET='add some secret here' -# Reccomended to be true, set to false if you are using http -# Note: Some auth providers may not support http requests -ALLOW_SECURE_COOKIES=true - # Sensitive Data Encryption Key while storing in Database (32 character) DATA_ENCRYPTION_KEY="data encryption key with 32 char" -# Hoppscotch App Domain Config -REDIRECT_URL="http://localhost:3000" # Whitelisted origins for the Hoppscotch App. # This list controls which origins can interact with the app through cross-origin comms. # - localhost ports (3170, 3000, 3100): app, backend, development servers and services @@ -28,50 +13,10 @@ REDIRECT_URL="http://localhost:3000" # NOT where the app runs. The app itself uses the `app://` protocol with dynamic # bundle names like `app://{bundle-name}/` WHITELISTED_ORIGINS="http://localhost:3170,http://localhost:3000,http://localhost:3100,app://localhost_3200,app://hoppscotch" -VITE_ALLOWED_AUTH_PROVIDERS=GOOGLE,GITHUB,MICROSOFT,EMAIL - -# Google Auth Config -GOOGLE_CLIENT_ID="************************************************" -GOOGLE_CLIENT_SECRET="************************************************" -GOOGLE_CALLBACK_URL="http://localhost:3170/v1/auth/google/callback" -GOOGLE_SCOPE="email,profile" - -# Github Auth Config -GITHUB_CLIENT_ID="************************************************" -GITHUB_CLIENT_SECRET="************************************************" -GITHUB_CALLBACK_URL="http://localhost:3170/v1/auth/github/callback" -GITHUB_SCOPE="user:email" - -# Microsoft Auth Config -MICROSOFT_CLIENT_ID="************************************************" -MICROSOFT_CLIENT_SECRET="************************************************" -MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback" -MICROSOFT_SCOPE="user.read" -MICROSOFT_TENANT="common" - -# Mailer config -MAILER_SMTP_ENABLE="true" -MAILER_USE_CUSTOM_CONFIGS="false" -MAILER_ADDRESS_FROM='"From Name Here" ' - -MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com" # used if custom mailer configs is false - -# The following are used if custom mailer configs is true -MAILER_SMTP_HOST="smtp.domain.com" -MAILER_SMTP_PORT="587" -MAILER_SMTP_SECURE="true" -MAILER_SMTP_USER="user@domain.com" -MAILER_SMTP_PASSWORD="pass" -MAILER_TLS_REJECT_UNAUTHORIZED="true" - -# Rate Limit Config -RATE_LIMIT_TTL=60 # In seconds -RATE_LIMIT_MAX=100 # Max requests per IP #-----------------------Frontend Config------------------------------# - # Base URLs VITE_BASE_URL=http://localhost:3000 VITE_SHORTCODE_BASE_URL=http://localhost:3000 diff --git a/packages/hoppscotch-backend/package.json b/packages/hoppscotch-backend/package.json index adf03099..99f0f22f 100644 --- a/packages/hoppscotch-backend/package.json +++ b/packages/hoppscotch-backend/package.json @@ -61,6 +61,7 @@ "handlebars": "4.7.8", "io-ts": "2.2.22", "luxon": "3.7.1", + "morgan": "1.10.1", "nodemailer": "7.0.5", "passport": "0.7.0", "passport-github2": "0.1.12", diff --git a/packages/hoppscotch-backend/src/app.module.ts b/packages/hoppscotch-backend/src/app.module.ts index a4207651..52113c64 100644 --- a/packages/hoppscotch-backend/src/app.module.ts +++ b/packages/hoppscotch-backend/src/app.module.ts @@ -44,7 +44,6 @@ import { PubSubModule } from './pubsub/pubsub.module'; }), GraphQLModule.forRootAsync({ driver: ApolloDriver, - imports: [ConfigModule], inject: [ConfigService], useFactory: async (configService: ConfigService) => { return { @@ -92,12 +91,11 @@ import { PubSubModule } from './pubsub/pubsub.module'; }, }), ThrottlerModule.forRootAsync({ - imports: [ConfigModule], inject: [ConfigService], useFactory: async (configService: ConfigService) => [ { - ttl: +configService.get('RATE_LIMIT_TTL'), - limit: +configService.get('RATE_LIMIT_MAX'), + ttl: +configService.get('INFRA.RATE_LIMIT_TTL'), + limit: +configService.get('INFRA.RATE_LIMIT_MAX'), }, ], }), diff --git a/packages/hoppscotch-backend/src/auth/auth.controller.ts b/packages/hoppscotch-backend/src/auth/auth.controller.ts index 72420f4d..10c9be9c 100644 --- a/packages/hoppscotch-backend/src/auth/auth.controller.ts +++ b/packages/hoppscotch-backend/src/auth/auth.controller.ts @@ -76,7 +76,7 @@ export class AuthController { async verify(@Body() data: VerifyMagicDto, @Res() res: Response) { const authTokens = await this.authService.verifyMagicLinkTokens(data); if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left); - authCookieHandler(res, authTokens.right, false, null); + authCookieHandler(res, authTokens.right, false, null, this.configService); } /** @@ -95,7 +95,7 @@ export class AuthController { user, ); if (E.isLeft(newTokenPair)) throwHTTPErr(newTokenPair.left); - authCookieHandler(res, newTokenPair.right, false, null); + authCookieHandler(res, newTokenPair.right, false, null, this.configService); } /** @@ -121,6 +121,7 @@ export class AuthController { authTokens.right, true, req.authInfo.state.redirect_uri, + this.configService, ); } @@ -147,6 +148,7 @@ export class AuthController { authTokens.right, true, req.authInfo.state.redirect_uri, + this.configService, ); } @@ -173,6 +175,7 @@ export class AuthController { authTokens.right, true, req.authInfo.state.redirect_uri, + this.configService, ); } diff --git a/packages/hoppscotch-backend/src/auth/auth.module.ts b/packages/hoppscotch-backend/src/auth/auth.module.ts index 4d5eb170..27a76634 100644 --- a/packages/hoppscotch-backend/src/auth/auth.module.ts +++ b/packages/hoppscotch-backend/src/auth/auth.module.ts @@ -10,7 +10,7 @@ import { GoogleStrategy } from './strategies/google.strategy'; import { GithubStrategy } from './strategies/github.strategy'; import { MicrosoftStrategy } from './strategies/microsoft.strategy'; import { AuthProvider, authProviderCheck } from './helper'; -import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ConfigService } from '@nestjs/config'; import { getConfiguredSSOProvidersFromInfraConfig, isInfraConfigTablePopulated, @@ -22,15 +22,14 @@ import { InfraConfigModule } from 'src/infra-config/infra-config.module'; UserModule, PassportModule, JwtModule.registerAsync({ - imports: [ConfigModule], inject: [ConfigService], useFactory: async (configService: ConfigService) => ({ - secret: configService.get('JWT_SECRET'), + secret: configService.get('INFRA.JWT_SECRET'), }), }), InfraConfigModule, ], - providers: [AuthService, JwtStrategy, RTJwtStrategy], + providers: [AuthService], controllers: [AuthController], }) export class AuthModule { @@ -57,7 +56,7 @@ export class AuthModule { return { module: AuthModule, - providers, + providers: [...providers, JwtStrategy, RTJwtStrategy], }; } } diff --git a/packages/hoppscotch-backend/src/auth/auth.service.ts b/packages/hoppscotch-backend/src/auth/auth.service.ts index b0f8df4f..41109d8b 100644 --- a/packages/hoppscotch-backend/src/auth/auth.service.ts +++ b/packages/hoppscotch-backend/src/auth/auth.service.ts @@ -50,11 +50,13 @@ export class AuthService { */ private async generateMagicLinkTokens(user: AuthUser) { const salt = await bcrypt.genSalt( - parseInt(this.configService.get('TOKEN_SALT_COMPLEXITY')), + parseInt(this.configService.get('INFRA.TOKEN_SALT_COMPLEXITY')), ); const expiresOn = DateTime.now() .plus({ - hours: parseInt(this.configService.get('MAGIC_LINK_TOKEN_VALIDITY')), + hours: parseInt( + this.configService.get('INFRA.MAGIC_LINK_TOKEN_VALIDITY'), + ), }) .toISO() .toString(); @@ -106,7 +108,7 @@ export class AuthService { }; const refreshToken = await this.jwtService.sign(refreshTokenPayload, { - expiresIn: this.configService.get('REFRESH_TOKEN_VALIDITY'), //7 Days + expiresIn: this.configService.get('INFRA.REFRESH_TOKEN_VALIDITY'), //7 Days }); const refreshTokenHash = await argon2.hash(refreshToken); @@ -142,7 +144,7 @@ export class AuthService { return E.right({ access_token: await this.jwtService.sign(accessTokenPayload, { - expiresIn: this.configService.get('ACCESS_TOKEN_VALIDITY'), //1 Day + expiresIn: this.configService.get('INFRA.ACCESS_TOKEN_VALIDITY'), //1 Day }), refresh_token: refreshToken.right, }); diff --git a/packages/hoppscotch-backend/src/auth/helper.ts b/packages/hoppscotch-backend/src/auth/helper.ts index d67f9373..d76207ef 100644 --- a/packages/hoppscotch-backend/src/auth/helper.ts +++ b/packages/hoppscotch-backend/src/auth/helper.ts @@ -41,30 +41,29 @@ export const authCookieHandler = ( authTokens: AuthTokens, redirect: boolean, redirectUrl: string | null, + configService: ConfigService, ) => { - const configService = new ConfigService(); - const currentTime = DateTime.now(); const accessTokenValidity = currentTime .plus({ - milliseconds: parseInt(configService.get('ACCESS_TOKEN_VALIDITY')), + milliseconds: parseInt(configService.get('INFRA.ACCESS_TOKEN_VALIDITY')), }) .toMillis(); const refreshTokenValidity = currentTime .plus({ - milliseconds: parseInt(configService.get('REFRESH_TOKEN_VALIDITY')), + milliseconds: parseInt(configService.get('INFRA.REFRESH_TOKEN_VALIDITY')), }) .toMillis(); res.cookie(AuthTokenType.ACCESS_TOKEN, authTokens.access_token, { httpOnly: true, - secure: configService.get('ALLOW_SECURE_COOKIES') === 'true', + secure: configService.get('INFRA.ALLOW_SECURE_COOKIES') === 'true', sameSite: 'lax', maxAge: accessTokenValidity, }); res.cookie(AuthTokenType.REFRESH_TOKEN, authTokens.refresh_token, { httpOnly: true, - secure: configService.get('ALLOW_SECURE_COOKIES') === 'true', + secure: configService.get('INFRA.ALLOW_SECURE_COOKIES') === 'true', sameSite: 'lax', maxAge: refreshTokenValidity, }); @@ -74,12 +73,11 @@ export const authCookieHandler = ( } // check to see if redirectUrl is a whitelisted url - const whitelistedOrigins = configService - .get('WHITELISTED_ORIGINS') - .split(','); + const whitelistedOrigins = + configService.get('WHITELISTED_ORIGINS')?.split(',') ?? []; if (!whitelistedOrigins.includes(redirectUrl)) - // if it is not redirect by default to REDIRECT_URL - redirectUrl = configService.get('REDIRECT_URL'); + // if it is not redirect by default to App + redirectUrl = configService.get('VITE_BASE_URL'); return res.status(HttpStatus.OK).redirect(redirectUrl); }; @@ -121,11 +119,7 @@ export function authProviderCheck( throwErr(AUTH_PROVIDER_NOT_SPECIFIED); } - const envVariables = VITE_ALLOWED_AUTH_PROVIDERS - ? VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) => - provider.trim().toUpperCase(), - ) - : []; + const envVariables = VITE_ALLOWED_AUTH_PROVIDERS?.split(',') ?? []; if (!envVariables.includes(provider.toUpperCase())) return false; diff --git a/packages/hoppscotch-backend/src/auth/strategies/jwt.strategy.ts b/packages/hoppscotch-backend/src/auth/strategies/jwt.strategy.ts index 322ebef6..3b9b97c5 100644 --- a/packages/hoppscotch-backend/src/auth/strategies/jwt.strategy.ts +++ b/packages/hoppscotch-backend/src/auth/strategies/jwt.strategy.ts @@ -93,7 +93,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { ), ), ]), - secretOrKey: configService.get('JWT_SECRET'), + secretOrKey: configService.get('INFRA.JWT_SECRET'), }); } diff --git a/packages/hoppscotch-backend/src/auth/strategies/rt-jwt.strategy.ts b/packages/hoppscotch-backend/src/auth/strategies/rt-jwt.strategy.ts index e8c57ab8..e87725cf 100644 --- a/packages/hoppscotch-backend/src/auth/strategies/rt-jwt.strategy.ts +++ b/packages/hoppscotch-backend/src/auth/strategies/rt-jwt.strategy.ts @@ -33,7 +33,7 @@ export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { return RTCookie; }, ]), - secretOrKey: configService.get('JWT_SECRET'), + secretOrKey: configService.get('INFRA.JWT_SECRET'), }); } diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index 1d33a1c1..e06f85e4 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -36,13 +36,6 @@ export const JSON_INVALID = 'json_invalid'; */ export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified'; -/** - * Auth Provider not specified - * (Auth) - */ -export const AUTH_PROVIDER_NOT_CONFIGURED = - 'auth/provider_not_configured_correctly'; - /** * Email not provided by OAuth provider * (SSO Strategies) @@ -50,12 +43,6 @@ export const AUTH_PROVIDER_NOT_CONFIGURED = export const AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH = 'auth/email_not_provided_by_oauth'; -/** - * Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file - */ -export const ENV_NOT_FOUND_KEY_AUTH_PROVIDERS = - '"VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file'; - /** * Environment variable "DATA_ENCRYPTION_KEY" is not present in .env file */ @@ -68,18 +55,6 @@ export const ENV_NOT_FOUND_KEY_DATA_ENCRYPTION_KEY = export const ENV_INVALID_DATA_ENCRYPTION_KEY = '"DATA_ENCRYPTION_KEY" value changed in .env file. Please undo the changes and restart the server'; -/** - * Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file - */ -export const ENV_EMPTY_AUTH_PROVIDERS = - '"VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file'; - -/** - * Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file - */ -export const ENV_NOT_SUPPORT_AUTH_PROVIDERS = - '"VITE_ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file'; - /** * Tried to delete a user data document from fb firestore but failed. * (FirebaseService) diff --git a/packages/hoppscotch-backend/src/infra-config/dto/onboarding.dto.ts b/packages/hoppscotch-backend/src/infra-config/dto/onboarding.dto.ts new file mode 100644 index 00000000..f5613cfc --- /dev/null +++ b/packages/hoppscotch-backend/src/infra-config/dto/onboarding.dto.ts @@ -0,0 +1,200 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { IsOptional, IsString } from 'class-validator'; +import { InfraConfigEnum } from 'src/types/InfraConfig'; + +export class GetOnboardingStatusResponse { + @ApiProperty() + @Expose() + onboardingCompleted: boolean; + @ApiProperty() + @Expose() + canReRunOnboarding: boolean; +} + +export class SaveOnboardingConfigRequest { + @ApiProperty() + @IsString() + [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS]: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.GOOGLE_CLIENT_ID]: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.GOOGLE_CLIENT_SECRET]: string; + @ApiPropertyOptional() + @IsOptional() + [InfraConfigEnum.GOOGLE_CALLBACK_URL]: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.GOOGLE_SCOPE]: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.GITHUB_CLIENT_ID]: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.GITHUB_CLIENT_SECRET]: string; + @ApiPropertyOptional() + @IsOptional() + [InfraConfigEnum.GITHUB_CALLBACK_URL]: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.GITHUB_SCOPE]: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.MICROSOFT_CLIENT_ID]: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.MICROSOFT_CLIENT_SECRET]: string; + @ApiPropertyOptional() + @IsOptional() + [InfraConfigEnum.MICROSOFT_CALLBACK_URL]: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.MICROSOFT_SCOPE]: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.MICROSOFT_TENANT]: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.MAILER_SMTP_ENABLE]: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS]: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.MAILER_ADDRESS_FROM]: string; + + @ApiPropertyOptional() + @IsOptional() + [InfraConfigEnum.MAILER_SMTP_URL]: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.MAILER_SMTP_HOST]: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.MAILER_SMTP_PORT]: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.MAILER_SMTP_SECURE]: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.MAILER_SMTP_USER]: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.MAILER_SMTP_PASSWORD]: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + [InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED]: string; +} + +export class SaveOnboardingConfigResponse { + @ApiProperty() + @Expose() + token: string; +} + +export class GetOnboardingConfigResponse { + @ApiProperty() + @Expose() + [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS]: string; + + @ApiProperty({ default: null }) + @Expose() + [InfraConfigEnum.GOOGLE_CLIENT_ID]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.GOOGLE_CLIENT_SECRET]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.GOOGLE_CALLBACK_URL]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.GOOGLE_SCOPE]: string; + + @ApiProperty() + @Expose() + [InfraConfigEnum.GITHUB_CLIENT_ID]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.GITHUB_CLIENT_SECRET]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.GITHUB_CALLBACK_URL]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.GITHUB_SCOPE]: string; + + @ApiProperty() + @Expose() + [InfraConfigEnum.MICROSOFT_CLIENT_ID]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.MICROSOFT_CLIENT_SECRET]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.MICROSOFT_CALLBACK_URL]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.MICROSOFT_SCOPE]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.MICROSOFT_TENANT]: string; + + @ApiProperty() + @Expose() + [InfraConfigEnum.MAILER_SMTP_ENABLE]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.MAILER_ADDRESS_FROM]: string; + + @ApiProperty() + @Expose() + [InfraConfigEnum.MAILER_SMTP_URL]: string; + + @ApiProperty() + @Expose() + [InfraConfigEnum.MAILER_SMTP_HOST]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.MAILER_SMTP_PORT]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.MAILER_SMTP_SECURE]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.MAILER_SMTP_USER]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.MAILER_SMTP_PASSWORD]: string; + @ApiProperty() + @Expose() + [InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED]: string; +} diff --git a/packages/hoppscotch-backend/src/infra-config/helper.ts b/packages/hoppscotch-backend/src/infra-config/helper.ts index 8e9101bd..ce0b05d0 100644 --- a/packages/hoppscotch-backend/src/infra-config/helper.ts +++ b/packages/hoppscotch-backend/src/infra-config/helper.ts @@ -1,13 +1,9 @@ import { AuthProvider } from 'src/auth/helper'; -import { - AUTH_PROVIDER_NOT_CONFIGURED, - ENV_INVALID_DATA_ENCRYPTION_KEY, -} from 'src/errors'; +import { ENV_INVALID_DATA_ENCRYPTION_KEY } from 'src/errors'; import { PrismaService } from 'src/prisma/prisma.service'; import { InfraConfigEnum } from 'src/types/InfraConfig'; -import { decrypt, encrypt, throwErr } from 'src/utils'; +import { decrypt, encrypt } from 'src/utils'; import { randomBytes } from 'crypto'; -import { InfraConfig } from '@prisma/client'; export enum ServiceStatus { ENABLE = 'ENABLE', @@ -17,43 +13,52 @@ export enum ServiceStatus { type DefaultInfraConfig = { name: InfraConfigEnum; value: string; - lastSyncedEnvFileValue: string; isEncrypted: boolean; }; -const AuthProviderConfigurations = { - [AuthProvider.GOOGLE]: [ - InfraConfigEnum.GOOGLE_CLIENT_ID, - InfraConfigEnum.GOOGLE_CLIENT_SECRET, - InfraConfigEnum.GOOGLE_CALLBACK_URL, - InfraConfigEnum.GOOGLE_SCOPE, - ], - [AuthProvider.GITHUB]: [ - InfraConfigEnum.GITHUB_CLIENT_ID, - InfraConfigEnum.GITHUB_CLIENT_SECRET, - InfraConfigEnum.GITHUB_CALLBACK_URL, - InfraConfigEnum.GITHUB_SCOPE, - ], - [AuthProvider.MICROSOFT]: [ - InfraConfigEnum.MICROSOFT_CLIENT_ID, - InfraConfigEnum.MICROSOFT_CLIENT_SECRET, - InfraConfigEnum.MICROSOFT_CALLBACK_URL, - InfraConfigEnum.MICROSOFT_SCOPE, - InfraConfigEnum.MICROSOFT_TENANT, - ], - [AuthProvider.EMAIL]: - process.env.MAILER_USE_CUSTOM_CONFIGS === 'true' - ? [ - InfraConfigEnum.MAILER_SMTP_HOST, - InfraConfigEnum.MAILER_SMTP_PORT, - InfraConfigEnum.MAILER_SMTP_SECURE, - InfraConfigEnum.MAILER_SMTP_USER, - InfraConfigEnum.MAILER_SMTP_PASSWORD, - InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED, - InfraConfigEnum.MAILER_ADDRESS_FROM, - ] - : [InfraConfigEnum.MAILER_SMTP_URL, InfraConfigEnum.MAILER_ADDRESS_FROM], -}; +/** + * Returns a mapping of authentication providers to their required configuration keys based on the current environment configuration. + */ +export function getAuthProviderRequiredKeys( + env: Record, +): Record { + return { + [AuthProvider.GOOGLE]: [ + InfraConfigEnum.GOOGLE_CLIENT_ID, + InfraConfigEnum.GOOGLE_CLIENT_SECRET, + InfraConfigEnum.GOOGLE_CALLBACK_URL, + InfraConfigEnum.GOOGLE_SCOPE, + ], + [AuthProvider.GITHUB]: [ + InfraConfigEnum.GITHUB_CLIENT_ID, + InfraConfigEnum.GITHUB_CLIENT_SECRET, + InfraConfigEnum.GITHUB_CALLBACK_URL, + InfraConfigEnum.GITHUB_SCOPE, + ], + [AuthProvider.MICROSOFT]: [ + InfraConfigEnum.MICROSOFT_CLIENT_ID, + InfraConfigEnum.MICROSOFT_CLIENT_SECRET, + InfraConfigEnum.MICROSOFT_CALLBACK_URL, + InfraConfigEnum.MICROSOFT_SCOPE, + InfraConfigEnum.MICROSOFT_TENANT, + ], + [AuthProvider.EMAIL]: + env['INFRA'].MAILER_USE_CUSTOM_CONFIGS === 'true' + ? [ + InfraConfigEnum.MAILER_SMTP_HOST, + InfraConfigEnum.MAILER_SMTP_PORT, + InfraConfigEnum.MAILER_SMTP_SECURE, + InfraConfigEnum.MAILER_SMTP_USER, + InfraConfigEnum.MAILER_SMTP_PASSWORD, + InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED, + InfraConfigEnum.MAILER_ADDRESS_FROM, + ] + : [ + InfraConfigEnum.MAILER_SMTP_URL, + InfraConfigEnum.MAILER_ADDRESS_FROM, + ], + }; +} /** * Load environment variables from the database and set them in the process @@ -64,10 +69,9 @@ const AuthProviderConfigurations = { export async function loadInfraConfiguration() { try { const prisma = new PrismaService(); - const infraConfigs = await prisma.infraConfig.findMany(); - const environmentObject: Record = {}; + const environmentObject: Record = {}; infraConfigs.forEach((infraConfig) => { if (infraConfig.isEncrypted) { environmentObject[infraConfig.name] = decrypt(infraConfig.value); @@ -83,6 +87,7 @@ export async function loadInfraConfiguration() { // Prisma throw error if 'Can't reach at database server' OR 'Table does not exist' // Reason for not throwing error is, we want successful build during 'postinstall' and generate dist files + console.log('Error from loadInfraConfiguration', error); return { INFRA: {} }; } } @@ -95,176 +100,206 @@ export async function getDefaultInfraConfigs(): Promise { const prisma = new PrismaService(); // Prepare rows for 'infra_config' table with default values (from .env) for each 'name' - const configuredSSOProviders = getConfiguredSSOProvidersFromEnvFile(); + const onboardingCompleteStatus = await isOnboardingCompleted(); const generatedAnalyticsUserId = generateAnalyticsUserId(); + const isSecureCookies = determineAllowSecureCookies( + process.env.VITE_BASE_URL, + ); const infraConfigDefaultObjs: DefaultInfraConfig[] = [ + { + name: InfraConfigEnum.ONBOARDING_COMPLETED, + value: onboardingCompleteStatus.toString(), + isEncrypted: false, + }, + { + name: InfraConfigEnum.ONBOARDING_RECOVERY_TOKEN, + value: null, + isEncrypted: false, + }, + { + name: InfraConfigEnum.JWT_SECRET, + value: encrypt(randomBytes(32).toString('hex')), + isEncrypted: true, + }, + { + name: InfraConfigEnum.SESSION_SECRET, + value: encrypt(randomBytes(32).toString('hex')), + isEncrypted: true, + }, + { + name: InfraConfigEnum.TOKEN_SALT_COMPLEXITY, + value: '10', + isEncrypted: false, + }, + { + name: InfraConfigEnum.MAGIC_LINK_TOKEN_VALIDITY, + value: '24', // 24 hours + isEncrypted: false, + }, + { + name: InfraConfigEnum.REFRESH_TOKEN_VALIDITY, + value: '604800000', // 7 days in milliseconds + isEncrypted: false, + }, + { + name: InfraConfigEnum.ACCESS_TOKEN_VALIDITY, + value: '86400000', // 1 day in milliseconds + isEncrypted: false, + }, + { + name: InfraConfigEnum.ALLOW_SECURE_COOKIES, + value: isSecureCookies.toString(), + isEncrypted: false, + }, + { + name: InfraConfigEnum.RATE_LIMIT_TTL, + value: '10000', // in milliseconds (10 seconds) + isEncrypted: false, + }, + { + name: InfraConfigEnum.RATE_LIMIT_MAX, + value: '100', // 100 requests per IP per RATE_LIMIT_TTL + isEncrypted: false, + }, { name: InfraConfigEnum.MAILER_SMTP_ENABLE, - value: process.env.MAILER_SMTP_ENABLE ?? 'true', - lastSyncedEnvFileValue: process.env.MAILER_SMTP_ENABLE ?? 'true', + value: 'false', isEncrypted: false, }, { name: InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS, - value: process.env.MAILER_USE_CUSTOM_CONFIGS ?? 'false', - lastSyncedEnvFileValue: process.env.MAILER_USE_CUSTOM_CONFIGS ?? 'false', + value: null, isEncrypted: false, }, { name: InfraConfigEnum.MAILER_SMTP_URL, - value: encrypt(process.env.MAILER_SMTP_URL), - lastSyncedEnvFileValue: encrypt(process.env.MAILER_SMTP_URL), + value: null, isEncrypted: true, }, { name: InfraConfigEnum.MAILER_ADDRESS_FROM, - value: process.env.MAILER_ADDRESS_FROM, - lastSyncedEnvFileValue: process.env.MAILER_ADDRESS_FROM, + value: null, isEncrypted: false, }, { name: InfraConfigEnum.MAILER_SMTP_HOST, - value: process.env.MAILER_SMTP_HOST, - lastSyncedEnvFileValue: process.env.MAILER_SMTP_HOST, + value: null, isEncrypted: false, }, { name: InfraConfigEnum.MAILER_SMTP_PORT, - value: process.env.MAILER_SMTP_PORT, - lastSyncedEnvFileValue: process.env.MAILER_SMTP_PORT, + value: null, isEncrypted: false, }, { name: InfraConfigEnum.MAILER_SMTP_SECURE, - value: process.env.MAILER_SMTP_SECURE, - lastSyncedEnvFileValue: process.env.MAILER_SMTP_SECURE, + value: null, isEncrypted: false, }, { name: InfraConfigEnum.MAILER_SMTP_USER, - value: process.env.MAILER_SMTP_USER, - lastSyncedEnvFileValue: process.env.MAILER_SMTP_USER, + value: null, isEncrypted: false, }, { name: InfraConfigEnum.MAILER_SMTP_PASSWORD, - value: encrypt(process.env.MAILER_SMTP_PASSWORD), - lastSyncedEnvFileValue: encrypt(process.env.MAILER_SMTP_PASSWORD), + value: null, isEncrypted: true, }, { name: InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED, - value: process.env.MAILER_TLS_REJECT_UNAUTHORIZED, - lastSyncedEnvFileValue: process.env.MAILER_TLS_REJECT_UNAUTHORIZED, + value: null, isEncrypted: false, }, { name: InfraConfigEnum.GOOGLE_CLIENT_ID, - value: encrypt(process.env.GOOGLE_CLIENT_ID), - lastSyncedEnvFileValue: encrypt(process.env.GOOGLE_CLIENT_ID), + value: null, isEncrypted: true, }, { name: InfraConfigEnum.GOOGLE_CLIENT_SECRET, - value: encrypt(process.env.GOOGLE_CLIENT_SECRET), - lastSyncedEnvFileValue: encrypt(process.env.GOOGLE_CLIENT_SECRET), + value: null, isEncrypted: true, }, { name: InfraConfigEnum.GOOGLE_CALLBACK_URL, - value: process.env.GOOGLE_CALLBACK_URL, - lastSyncedEnvFileValue: process.env.GOOGLE_CALLBACK_URL, + value: null, isEncrypted: false, }, { name: InfraConfigEnum.GOOGLE_SCOPE, - value: process.env.GOOGLE_SCOPE, - lastSyncedEnvFileValue: process.env.GOOGLE_SCOPE, + value: null, isEncrypted: false, }, { name: InfraConfigEnum.GITHUB_CLIENT_ID, - value: encrypt(process.env.GITHUB_CLIENT_ID), - lastSyncedEnvFileValue: encrypt(process.env.GITHUB_CLIENT_ID), + value: null, isEncrypted: true, }, { name: InfraConfigEnum.GITHUB_CLIENT_SECRET, - value: encrypt(process.env.GITHUB_CLIENT_SECRET), - lastSyncedEnvFileValue: encrypt(process.env.GITHUB_CLIENT_SECRET), + value: null, isEncrypted: true, }, { name: InfraConfigEnum.GITHUB_CALLBACK_URL, - value: process.env.GITHUB_CALLBACK_URL, - lastSyncedEnvFileValue: process.env.GITHUB_CALLBACK_URL, + value: null, isEncrypted: false, }, { name: InfraConfigEnum.GITHUB_SCOPE, - value: process.env.GITHUB_SCOPE, - lastSyncedEnvFileValue: process.env.GITHUB_SCOPE, + value: null, isEncrypted: false, }, { name: InfraConfigEnum.MICROSOFT_CLIENT_ID, - value: encrypt(process.env.MICROSOFT_CLIENT_ID), - lastSyncedEnvFileValue: encrypt(process.env.MICROSOFT_CLIENT_ID), + value: null, isEncrypted: true, }, { name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET, - value: encrypt(process.env.MICROSOFT_CLIENT_SECRET), - lastSyncedEnvFileValue: encrypt(process.env.MICROSOFT_CLIENT_SECRET), + value: null, isEncrypted: true, }, { name: InfraConfigEnum.MICROSOFT_CALLBACK_URL, - value: process.env.MICROSOFT_CALLBACK_URL, - lastSyncedEnvFileValue: process.env.MICROSOFT_CALLBACK_URL, + value: null, isEncrypted: false, }, { name: InfraConfigEnum.MICROSOFT_SCOPE, - value: process.env.MICROSOFT_SCOPE, - lastSyncedEnvFileValue: process.env.MICROSOFT_SCOPE, + value: null, isEncrypted: false, }, { name: InfraConfigEnum.MICROSOFT_TENANT, - value: process.env.MICROSOFT_TENANT, - lastSyncedEnvFileValue: process.env.MICROSOFT_TENANT, + value: null, isEncrypted: false, }, { name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS, - value: configuredSSOProviders, - lastSyncedEnvFileValue: configuredSSOProviders, + value: null, isEncrypted: false, }, { name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION, value: false.toString(), - lastSyncedEnvFileValue: null, isEncrypted: false, }, { name: InfraConfigEnum.ANALYTICS_USER_ID, value: generatedAnalyticsUserId, - lastSyncedEnvFileValue: null, isEncrypted: false, }, { name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP, value: (await prisma.infraConfig.count()) === 0 ? 'true' : 'false', - lastSyncedEnvFileValue: null, isEncrypted: false, }, { name: InfraConfigEnum.USER_HISTORY_STORE_ENABLED, value: 'true', - lastSyncedEnvFileValue: null, isEncrypted: false, }, ]; @@ -311,48 +346,6 @@ export async function getEncryptionRequiredInfraConfigEntries( return requiredEncryption; } -/** - * Sync the 'infra_config' table with .env file - * @returns Array of InfraConfig - */ -export async function syncInfraConfigWithEnvFile() { - const prisma = new PrismaService(); - const dbInfraConfigs = await prisma.infraConfig.findMany(); - - const updateRequiredObjs: (Partial & { id: string })[] = []; - - for (const dbConfig of dbInfraConfigs) { - const envValue = process.env[dbConfig.name]; - - // lastSyncedEnvFileValue null check for backward compatibility from 2024.10.2 and below - if (!dbConfig.lastSyncedEnvFileValue && envValue) { - const configValue = dbConfig.isEncrypted ? encrypt(envValue) : envValue; - updateRequiredObjs.push({ - id: dbConfig.id, - value: dbConfig.value === null ? configValue : undefined, - lastSyncedEnvFileValue: configValue, - }); - continue; - } - - // If the value in the database is different from the value in the .env file, means the value in the .env file has been updated - const rawLastSyncedEnvFileValue = dbConfig.isEncrypted - ? decrypt(dbConfig.lastSyncedEnvFileValue) - : dbConfig.lastSyncedEnvFileValue; - - if (rawLastSyncedEnvFileValue != envValue) { - const configValue = dbConfig.isEncrypted ? encrypt(envValue) : envValue; - updateRequiredObjs.push({ - id: dbConfig.id, - value: configValue ?? null, - lastSyncedEnvFileValue: configValue ?? null, - }); - } - } - - return updateRequiredObjs; -} - /** * Verify if 'infra_config' table is loaded with all entries * @returns boolean @@ -389,72 +382,31 @@ export function stopApp() { }, 5000); } -/** - * Get the configured SSO providers from .env file - * @description This function verify if the required parameters for each SSO provider are configured in .env file. Usage on first time setup and reset. - * @returns Array of configured SSO providers - */ -export function getConfiguredSSOProvidersFromEnvFile() { - const allowedAuthProviders: string[] = - process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(','); - const configuredAuthProviders: string[] = []; - - const addProviderIfConfigured = (provider) => { - const configParameters: string[] = AuthProviderConfigurations[provider]; - - const isConfigured = configParameters.every((configParameter) => { - return process.env[configParameter]; - }); - if (isConfigured) configuredAuthProviders.push(provider); - }; - - allowedAuthProviders.forEach((provider) => addProviderIfConfigured(provider)); - - if (configuredAuthProviders.length === 0) { - throwErr(AUTH_PROVIDER_NOT_CONFIGURED); - } else if (allowedAuthProviders.length !== configuredAuthProviders.length) { - const unConfiguredAuthProviders = allowedAuthProviders.filter( - (provider) => { - return !configuredAuthProviders.includes(provider); - }, - ); - console.log( - `${unConfiguredAuthProviders.join( - ',', - )} SSO auth provider(s) are not configured properly in .env file. Do configure them from Admin Dashboard.`, - ); - } - - return configuredAuthProviders.join(','); -} - /** * Get the configured SSO providers from 'infra_config' table. * @description Usage every time the app starts by AuthModule to initiate Strategies. * @returns Array of configured SSO providers */ export async function getConfiguredSSOProvidersFromInfraConfig() { + const prisma = new PrismaService(); const env = await loadInfraConfiguration(); + const providerConfigKeys = getAuthProviderRequiredKeys(env); const allowedAuthProviders: string[] = - env['INFRA'].VITE_ALLOWED_AUTH_PROVIDERS.split(','); - const configuredAuthProviders: string[] = []; + env['INFRA'].VITE_ALLOWED_AUTH_PROVIDERS?.split(',') ?? []; - const addProviderIfConfigured = (provider) => { - const configParameters: string[] = AuthProviderConfigurations[provider]; - - const isConfigured = configParameters.every((configParameter) => { - return env['INFRA'][configParameter]; - }); - if (isConfigured) configuredAuthProviders.push(provider); - }; - - allowedAuthProviders.forEach((provider) => addProviderIfConfigured(provider)); + const configuredAuthProviders = allowedAuthProviders.filter((provider) => { + const requiredKeys = providerConfigKeys[provider]; + return requiredKeys?.every((key) => env['INFRA'][key]); + }); if (configuredAuthProviders.length === 0) { + await prisma.infraConfig.update({ + where: { name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS }, + data: { value: null }, + }); return ''; } else if (allowedAuthProviders.length !== configuredAuthProviders.length) { - const prisma = new PrismaService(); await prisma.infraConfig.update({ where: { name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS }, data: { value: configuredAuthProviders.join(',') }, @@ -468,6 +420,24 @@ export async function getConfiguredSSOProvidersFromInfraConfig() { return configuredAuthProviders.join(','); } +/** + * Check if the onboarding is completed by verifying if the allowed auth providers are configured + * @returns boolean + */ +export async function isOnboardingCompleted(): Promise { + const prisma = new PrismaService(); + const allowedProviders = await prisma.infraConfig.findUnique({ + where: { name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS }, + select: { value: true }, + }); + + if (!allowedProviders?.value || allowedProviders.value === '') { + return false; + } + + return true; +} + /** * Generate a hashed valued for analytics * @returns Generated hashed value @@ -476,3 +446,58 @@ export function generateAnalyticsUserId() { const hashedUserID = randomBytes(20).toString('hex'); return hashedUserID; } + +/** + * Determine if ALLOW_SECURE_COOKIES should be true or false + * @returns boolean + */ +export function determineAllowSecureCookies(appBaseUrl: string) { + return appBaseUrl.startsWith('https'); +} + +/** + * Builds a map of environment variables that are derived from other configuration values + * @returns Record + */ +export async function buildDerivedEnv() { + const envConfigMap = await loadInfraConfiguration(); + const derivedEnv: Record = {}; + + // Normalize URLs + const baseUrl = process.env.VITE_BASE_URL || ''; + const backendUrl = process.env.VITE_BACKEND_API_URL || ''; + const normalizedBackendUrl = backendUrl?.replace(/\/+$/, ''); // remove trailing slash + + // Set ALLOW_SECURE_COOKIES based on base URL protocol + const expectedSecure = determineAllowSecureCookies(baseUrl).toString(); + const currentSecure = envConfigMap.INFRA.ALLOW_SECURE_COOKIES; + if (currentSecure !== expectedSecure) { + derivedEnv.ALLOW_SECURE_COOKIES = expectedSecure; + } + + // Set GOOGLE_CALLBACK_URL, MICROSOFT_CALLBACK_URL, and GITHUB_CALLBACK_URL based on backend URL (self healing) if user changed the backend URL + // Callback URL definitions + const callbackConfigs = [ + { key: InfraConfigEnum.GOOGLE_CALLBACK_URL, path: '/auth/google/callback' }, + { + key: InfraConfigEnum.MICROSOFT_CALLBACK_URL, + path: '/auth/microsoft/callback', + }, + { key: InfraConfigEnum.GITHUB_CALLBACK_URL, path: '/auth/github/callback' }, + ]; + // Update callback URLs if they don't match the backend + for (const { key, path } of callbackConfigs) { + const currentCallback = envConfigMap.INFRA[key]; + const expectedCallback = `${normalizedBackendUrl}${path}`; + + if ( + backendUrl.length > 0 && + currentCallback && + currentCallback !== expectedCallback + ) { + derivedEnv[key] = expectedCallback; + } + } + + return derivedEnv; +} diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.module.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.module.ts index 8839c996..7cae136c 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.module.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.module.ts @@ -2,10 +2,13 @@ import { Module } from '@nestjs/common'; import { InfraConfigService } from './infra-config.service'; import { SiteController } from './infra-config.controller'; import { InfraConfigResolver } from './infra-config.resolver'; +import { UserModule } from 'src/user/user.module'; +import { OnboardingController } from './onboarding.controller'; @Module({ + imports: [UserModule], + controllers: [SiteController, OnboardingController], providers: [InfraConfigResolver, InfraConfigService], exports: [InfraConfigService], - controllers: [SiteController], }) export class InfraConfigModule {} diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts index 7f0921ef..7d3a83d1 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts @@ -14,15 +14,18 @@ import { InfraConfig } from './infra-config.model'; import { PubSubService } from 'src/pubsub/pubsub.service'; import { ServiceStatus } from './helper'; import * as E from 'fp-ts/Either'; +import { UserService } from 'src/user/user.service'; const mockPrisma = mockDeep(); const mockConfigService = mockDeep(); const mockPubsub = mockDeep(); +const mockUserService = mockDeep(); const infraConfigService = new InfraConfigService( mockPrisma, mockConfigService, mockPubsub, + mockUserService, ); const INITIALIZED_DATE_CONST = new Date(); diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts index 820b07c3..52dca33d 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts @@ -25,15 +25,23 @@ import { import { ConfigService } from '@nestjs/config'; import { ServiceStatus, + buildDerivedEnv, getDefaultInfraConfigs, getEncryptionRequiredInfraConfigEntries, getMissingInfraConfigEntries, stopApp, - syncInfraConfigWithEnvFile, } from './helper'; import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args'; import { AuthProvider } from 'src/auth/helper'; import { PubSubService } from 'src/pubsub/pubsub.service'; +import { UserService } from 'src/user/user.service'; +import { + GetOnboardingConfigResponse, + GetOnboardingStatusResponse, + SaveOnboardingConfigRequest, + SaveOnboardingConfigResponse, +} from './dto/onboarding.dto'; +import * as crypto from 'crypto'; @Injectable() export class InfraConfigService implements OnModuleInit { @@ -41,6 +49,7 @@ export class InfraConfigService implements OnModuleInit { private readonly prisma: PrismaService, private readonly configService: ConfigService, private readonly pubsub: PubSubService, + private readonly userService: UserService, ) {} // Following fields are not updatable by `infraConfigs` Mutation. Use dedicated mutations for these fields instead. @@ -94,14 +103,14 @@ export class InfraConfigService implements OnModuleInit { await Promise.allSettled(dbOperations); } - // Sync the InfraConfigs with the .env file, if .env file updates later on - const envFileChangesRequired = await syncInfraConfigWithEnvFile(); - if (envFileChangesRequired.length > 0) { - const dbOperations = envFileChangesRequired.map((dbConfig) => { - const { id, ...dataObj } = dbConfig; + // Derive env variables programmatically if they don't exist or need to be updated + const derivedEnv = await buildDerivedEnv(); + + if (Object.keys(derivedEnv).length > 0) { + const dbOperations = Object.entries(derivedEnv).map(([name, value]) => { return this.prisma.infraConfig.update({ - where: { id: dbConfig.id }, - data: dataObj, + where: { name: name as InfraConfigEnum }, + data: { value }, }); }); await Promise.allSettled(dbOperations); @@ -111,7 +120,7 @@ export class InfraConfigService implements OnModuleInit { if ( propsToInsert.length > 0 || encryptionRequiredEntries.length > 0 || - envFileChangesRequired.length > 0 + Object.keys(derivedEnv).length > 0 ) { stopApp(); } @@ -210,10 +219,16 @@ export class InfraConfigService implements OnModuleInit { * @param infraConfigs InfraConfigs to update * @returns InfraConfig model */ - async updateMany(infraConfigs: InfraConfigArgs[]) { - for (let i = 0; i < infraConfigs.length; i++) { - if (this.EXCLUDE_FROM_UPDATE_CONFIGS.includes(infraConfigs[i].name)) - return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED); + async updateMany( + infraConfigs: InfraConfigArgs[], + checkDisallowedKeys: boolean = true, + ) { + if (checkDisallowedKeys) { + // Check if the names are allowed to update by client + for (let i = 0; i < infraConfigs.length; i++) { + if (this.EXCLUDE_FROM_UPDATE_CONFIGS.includes(infraConfigs[i].name)) + return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED); + } } const isValidate = this.validateEnvValues(infraConfigs); @@ -374,7 +389,7 @@ export class InfraConfigService implements OnModuleInit { const infra = await this.get(InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS); if (E.isLeft(infra)) return E.left(infra.left); - const allowedAuthProviders = infra.right.value.split(','); + const allowedAuthProviders = infra.right.value?.split(',') ?? []; let updatedAuthProviders = allowedAuthProviders; const infraConfigMap = await this.getInfraConfigsMap(); @@ -457,9 +472,11 @@ export class InfraConfigService implements OnModuleInit { * @returns string[] */ getAllowedAuthProviders() { - return this.configService - .get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS') - .split(','); + return ( + this.configService + .get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS') + ?.split(',') ?? [] + ); } /** @@ -485,6 +502,107 @@ export class InfraConfigService implements OnModuleInit { return E.right(infraConfig.right); } + /** + * Get onboarding status + * @returns GetOnboardingStatusResponse + */ + async getOnboardingStatus() { + const configMap = await this.getInfraConfigsMap(); + const usersCount = await this.userService.getUsersCount(); + + return E.right({ + onboardingCompleted: configMap.ONBOARDING_COMPLETED === 'true', + canReRunOnboarding: usersCount === 0, + } as GetOnboardingStatusResponse); + } + + /** + * Update the onboarding configuration + * @param dto SaveOnboardingConfigRequest + */ + async updateOnboardingConfig(dto: SaveOnboardingConfigRequest) { + const onboardingRecoveryToken = crypto.randomUUID(); + + const configEntries: InfraConfigArgs[] = [ + ...Object.entries(dto).map(([key, value]) => ({ + name: key as InfraConfigEnum, + value, + })), + { + name: InfraConfigEnum.ONBOARDING_COMPLETED, + value: 'true', + }, + { + name: InfraConfigEnum.ONBOARDING_RECOVERY_TOKEN, + value: onboardingRecoveryToken, + }, + ]; + + const isValidated = this.validateEnvValues(configEntries); + if (E.isLeft(isValidated)) return E.left(isValidated.left); + + // Verify MAILER_SMTP_ENABLE + if ( + dto[InfraConfigEnum.MAILER_SMTP_ENABLE] === 'true' && + !this.isServiceConfigured( + AuthProvider.EMAIL, + dto as unknown as Record, + ) + ) { + return E.left(INFRA_CONFIG_SERVICE_NOT_CONFIGURED); + } + + // Verify VITE_ALLOWED_AUTH_PROVIDERS + const allowedAuthProviders = + dto[InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS].split(','); + + if (allowedAuthProviders.length === 0) { + return E.left(AUTH_PROVIDER_NOT_SPECIFIED); + } + for (const provider of allowedAuthProviders) { + if ( + !Object.values(AuthProvider).includes(provider as AuthProvider) || + !this.isServiceConfigured( + provider as AuthProvider, + dto as unknown as Record, + ) + ) { + return E.left(INFRA_CONFIG_SERVICE_NOT_CONFIGURED); + } + } + + // Move forward with updating the InfraConfigs + const isUpdated = await this.updateMany(configEntries, false); + if (E.isLeft(isUpdated)) return E.left(isUpdated.left); + + return E.right({ + token: onboardingRecoveryToken, + } as SaveOnboardingConfigResponse); + } + + /** + * Get onboarding configuration + * @param token Onboarding recovery token + * @returns GetOnboardingConfigResponse + */ + async getOnboardingConfig(token: string) { + const configs = await this.getMany(Object.values(InfraConfigEnum), false); + if (E.isLeft(configs)) return E.left(configs.left); + + // Check if the onboarding recovery token is valid + const recoveryToken = configs.right.find( + (config) => config.name === InfraConfigEnum.ONBOARDING_RECOVERY_TOKEN, + )?.value; + const tokenIsValid = token === recoveryToken; + + const onboardingConfig = configs.right.reduce((acc, config) => { + acc[config.name] = tokenIsValid ? config.value : null; + return acc; + }, {} as GetOnboardingConfigResponse); + + return E.right(onboardingConfig); + } + /** * Reset all the InfraConfigs to their default values (from .env) */ @@ -530,98 +648,75 @@ export class InfraConfigService implements OnModuleInit { value: string; }[], ) { - for (let i = 0; i < infraConfigs.length; i++) { - switch (infraConfigs[i].name) { + for (const config of infraConfigs) { + const { name, value } = config; + + const fail = () => { + console.error(`[Infra Validation Failed] Key: ${name}`); + return E.left(INFRA_CONFIG_INVALID_INPUT); + }; + + switch (name) { case InfraConfigEnum.MAILER_SMTP_ENABLE: - if ( - infraConfigs[i].value !== 'true' && - infraConfigs[i].value !== 'false' - ) - return E.left(INFRA_CONFIG_INVALID_INPUT); - break; case InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS: - if ( - infraConfigs[i].value !== 'true' && - infraConfigs[i].value !== 'false' - ) - return E.left(INFRA_CONFIG_INVALID_INPUT); - break; - case InfraConfigEnum.MAILER_SMTP_URL: - const isValidUrl = validateSMTPUrl(infraConfigs[i].value); - if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; - case InfraConfigEnum.MAILER_ADDRESS_FROM: - const isValidEmail = validateSMTPEmail(infraConfigs[i].value); - if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; - case InfraConfigEnum.MAILER_SMTP_HOST: - if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; - case InfraConfigEnum.MAILER_SMTP_PORT: - if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; case InfraConfigEnum.MAILER_SMTP_SECURE: - if ( - infraConfigs[i].value !== 'true' && - infraConfigs[i].value !== 'false' - ) - return E.left(INFRA_CONFIG_INVALID_INPUT); - break; - case InfraConfigEnum.MAILER_SMTP_USER: - if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; - case InfraConfigEnum.MAILER_SMTP_PASSWORD: - if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; case InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED: - if ( - infraConfigs[i].value !== 'true' && - infraConfigs[i].value !== 'false' - ) - return E.left(INFRA_CONFIG_INVALID_INPUT); + if (value !== 'true' && value !== 'false') return fail(); break; + + case InfraConfigEnum.MAILER_SMTP_URL: + if (!validateSMTPUrl(value)) return fail(); + break; + + case InfraConfigEnum.MAILER_ADDRESS_FROM: + if (!validateSMTPEmail(value)) return fail(); + break; + + case InfraConfigEnum.MAILER_SMTP_HOST: + case InfraConfigEnum.MAILER_SMTP_PORT: + case InfraConfigEnum.MAILER_SMTP_USER: + case InfraConfigEnum.MAILER_SMTP_PASSWORD: case InfraConfigEnum.GOOGLE_CLIENT_ID: - if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; case InfraConfigEnum.GOOGLE_CLIENT_SECRET: - if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; - case InfraConfigEnum.GOOGLE_CALLBACK_URL: - if (!validateUrl(infraConfigs[i].value)) - return E.left(INFRA_CONFIG_INVALID_INPUT); - break; case InfraConfigEnum.GOOGLE_SCOPE: - if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; case InfraConfigEnum.GITHUB_CLIENT_ID: - if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; case InfraConfigEnum.GITHUB_CLIENT_SECRET: - if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; - case InfraConfigEnum.GITHUB_CALLBACK_URL: - if (!validateUrl(infraConfigs[i].value)) - return E.left(INFRA_CONFIG_INVALID_INPUT); - break; case InfraConfigEnum.GITHUB_SCOPE: - if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; case InfraConfigEnum.MICROSOFT_CLIENT_ID: - if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; case InfraConfigEnum.MICROSOFT_CLIENT_SECRET: - if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; - case InfraConfigEnum.MICROSOFT_CALLBACK_URL: - if (!validateUrl(infraConfigs[i].value)) - return E.left(INFRA_CONFIG_INVALID_INPUT); - break; case InfraConfigEnum.MICROSOFT_SCOPE: - if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); - break; case InfraConfigEnum.MICROSOFT_TENANT: - if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); + if (!value) return fail(); break; + + case InfraConfigEnum.GOOGLE_CALLBACK_URL: + case InfraConfigEnum.GITHUB_CALLBACK_URL: + case InfraConfigEnum.MICROSOFT_CALLBACK_URL: + if (!validateUrl(value)) return fail(); + break; + + case InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS: + const allowedAuthProviders = value.split(','); + if ( + allowedAuthProviders.length === 0 || + allowedAuthProviders.some( + (p) => !Object.values(AuthProvider).includes(p as AuthProvider), + ) + ) { + return fail(); + } + break; + + case InfraConfigEnum.TOKEN_SALT_COMPLEXITY: + case InfraConfigEnum.MAGIC_LINK_TOKEN_VALIDITY: + case InfraConfigEnum.ACCESS_TOKEN_VALIDITY: + case InfraConfigEnum.REFRESH_TOKEN_VALIDITY: + case InfraConfigEnum.RATE_LIMIT_TTL: + case InfraConfigEnum.RATE_LIMIT_MAX: + if (!Number.isInteger(Number(value)) || Number(value) < 1) + return fail(); + break; + default: break; } diff --git a/packages/hoppscotch-backend/src/infra-config/onboarding.controller.ts b/packages/hoppscotch-backend/src/infra-config/onboarding.controller.ts new file mode 100644 index 00000000..b931ba5f --- /dev/null +++ b/packages/hoppscotch-backend/src/infra-config/onboarding.controller.ts @@ -0,0 +1,96 @@ +import { Body, Controller, Get, HttpStatus, Post, Query } from '@nestjs/common'; +import { InfraConfigService } from './infra-config.service'; +import { RESTError } from 'src/types/RESTError'; +import { throwHTTPErr } from 'src/utils'; +import * as E from 'fp-ts/Either'; +import { + GetOnboardingConfigResponse, + GetOnboardingStatusResponse, + SaveOnboardingConfigRequest, + SaveOnboardingConfigResponse, +} from './dto/onboarding.dto'; +import { ApiCreatedResponse, ApiOkResponse } from '@nestjs/swagger'; +import { plainToInstance } from 'class-transformer'; + +@Controller({ path: 'onboarding', version: '1' }) +export class OnboardingController { + constructor(private infraConfigService: InfraConfigService) {} + + @Get('status') + @ApiOkResponse({ + description: 'Get onboarding status', + type: GetOnboardingStatusResponse, + }) + async getOnboardingStatus(): Promise { + const onboardingStatus = + await this.infraConfigService.getOnboardingStatus(); + + if (E.isLeft(onboardingStatus)) + throwHTTPErr({ + message: onboardingStatus.left, + statusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }); + + return plainToInstance( + GetOnboardingStatusResponse, + { + onboardingCompleted: onboardingStatus.right.onboardingCompleted, + canReRunOnboarding: onboardingStatus.right.canReRunOnboarding, + }, + { + excludeExtraneousValues: true, + enableImplicitConversion: true, + }, + ); + } + + @Post('config') + @ApiCreatedResponse({ + description: 'Onboarding configuration updated successfully', + type: SaveOnboardingConfigResponse, + }) + async updateOnboardingConfig(@Body() dto: SaveOnboardingConfigRequest) { + const updateConfigResult = + await this.infraConfigService.updateOnboardingConfig(dto); + + if (E.isLeft(updateConfigResult)) + throwHTTPErr({ + message: updateConfigResult.left, + statusCode: HttpStatus.BAD_REQUEST, + }); + + return plainToInstance( + SaveOnboardingConfigResponse, + updateConfigResult.right, + { + excludeExtraneousValues: true, + enableImplicitConversion: true, + }, + ); + } + + @Get('config') + @ApiOkResponse({ + description: 'Get onboarding configuration', + type: GetOnboardingConfigResponse, + }) + async getOnboardingConfig(@Query('token') token: string) { + const onboardingConfig = + await this.infraConfigService.getOnboardingConfig(token); + + if (E.isLeft(onboardingConfig)) + throwHTTPErr({ + message: onboardingConfig.left, + statusCode: HttpStatus.BAD_REQUEST, + }); + + return plainToInstance( + GetOnboardingConfigResponse, + onboardingConfig.right, + { + excludeExtraneousValues: true, + enableImplicitConversion: true, + }, + ); + } +} diff --git a/packages/hoppscotch-backend/src/mailer/helper.ts b/packages/hoppscotch-backend/src/mailer/helper.ts index e5629418..0fc0eeda 100644 --- a/packages/hoppscotch-backend/src/mailer/helper.ts +++ b/packages/hoppscotch-backend/src/mailer/helper.ts @@ -10,49 +10,33 @@ function isSMTPCustomConfigsEnabled(value) { return value === 'true'; } -export function getMailerAddressFrom(env, config): string { - return ( - env.INFRA.MAILER_ADDRESS_FROM ?? - config.get('MAILER_ADDRESS_FROM') ?? - throwErr(MAILER_SMTP_URL_UNDEFINED) - ); +export function getMailerAddressFrom(env): string { + return env.INFRA.MAILER_ADDRESS_FROM ?? throwErr(MAILER_SMTP_URL_UNDEFINED); } -export function getTransportOption(env, config): TransportType { +export function getTransportOption(env): TransportType { const useCustomConfigs = isSMTPCustomConfigsEnabled( - env.INFRA.MAILER_USE_CUSTOM_CONFIGS ?? - config.get('MAILER_USE_CUSTOM_CONFIGS'), + env.INFRA.MAILER_USE_CUSTOM_CONFIGS, ); if (!useCustomConfigs) { console.log('Using simple mailer configuration'); - return ( - env.INFRA.MAILER_SMTP_URL ?? - config.get('MAILER_SMTP_URL') ?? - throwErr(MAILER_SMTP_URL_UNDEFINED) - ); + return env.INFRA.MAILER_SMTP_URL ?? throwErr(MAILER_SMTP_URL_UNDEFINED); } else { console.log('Using advanced mailer configuration'); return { - host: env.INFRA.MAILER_SMTP_HOST ?? config.get('MAILER_SMTP_HOST'), - port: +(env.INFRA.MAILER_SMTP_PORT ?? config.get('MAILER_SMTP_PORT')), - secure: - (env.INFRA.MAILER_SMTP_SECURE ?? config.get('MAILER_SMTP_SECURE')) === - 'true', + host: env.INFRA.MAILER_SMTP_HOST, + port: +env.INFRA.MAILER_SMTP_PORT, + secure: env.INFRA.MAILER_SMTP_SECURE === 'true', auth: { user: - env.INFRA.MAILER_SMTP_USER ?? - config.get('MAILER_SMTP_USER') ?? - throwErr(MAILER_SMTP_USER_UNDEFINED), + env.INFRA.MAILER_SMTP_USER ?? throwErr(MAILER_SMTP_USER_UNDEFINED), pass: env.INFRA.MAILER_SMTP_PASSWORD ?? - config.get('MAILER_SMTP_PASSWORD') ?? throwErr(MAILER_SMTP_PASSWORD_UNDEFINED), }, tls: { - rejectUnauthorized: - (env.INFRA.MAILER_TLS_REJECT_UNAUTHORIZED ?? - config.get('MAILER_TLS_REJECT_UNAUTHORIZED')) === 'true', + rejectUnauthorized: env.INFRA.MAILER_TLS_REJECT_UNAUTHORIZED === 'true', }, }; } diff --git a/packages/hoppscotch-backend/src/mailer/mailer.module.ts b/packages/hoppscotch-backend/src/mailer/mailer.module.ts index cbe74345..e614f1b5 100644 --- a/packages/hoppscotch-backend/src/mailer/mailer.module.ts +++ b/packages/hoppscotch-backend/src/mailer/mailer.module.ts @@ -2,7 +2,6 @@ import { Global, Module } from '@nestjs/common'; import { MailerModule as NestMailerModule } from '@nestjs-modules/mailer'; import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; import { MailerService } from './mailer.service'; -import { ConfigService } from '@nestjs/config'; import { loadInfraConfiguration } from 'src/infra-config/helper'; import { getMailerAddressFrom, getTransportOption } from './helper'; @@ -14,7 +13,6 @@ import { getMailerAddressFrom, getTransportOption } from './helper'; }) export class MailerModule { static async register() { - const config = new ConfigService(); const env = await loadInfraConfiguration(); // If mailer SMTP is DISABLED, return the module without any configuration (service, listener, etc.) @@ -28,9 +26,9 @@ export class MailerModule { // If mailer is ENABLED, return the module with configuration (service, etc.) // Determine transport configuration based on custom config flag - const transportOption = getTransportOption(env, config); + const transportOption = getTransportOption(env); // Get mailer address from environment or config - const mailerAddressFrom = getMailerAddressFrom(env, config); + const mailerAddressFrom = getMailerAddressFrom(env); return { module: MailerModule, diff --git a/packages/hoppscotch-backend/src/main.ts b/packages/hoppscotch-backend/src/main.ts index 442aa408..de686b52 100644 --- a/packages/hoppscotch-backend/src/main.ts +++ b/packages/hoppscotch-backend/src/main.ts @@ -2,16 +2,16 @@ import { NestFactory } from '@nestjs/core'; import { json } from 'express'; import { AppModule } from './app.module'; import * as cookieParser from 'cookie-parser'; -import * as crypto from 'crypto'; import { ValidationPipe, VersioningType } from '@nestjs/common'; import * as session from 'express-session'; import { emitGQLSchemaFile } from './gql-schema'; -import { checkEnvironmentAuthProvider } from './utils'; +import * as crypto from 'crypto'; +import * as morgan from 'morgan'; import { ConfigService } from '@nestjs/config'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { InfraTokenModule } from './infra-token/infra-token.module'; -function setupSwagger(app) { +function setupSwagger(app, isProduction: boolean) { const swaggerDocPath = '/api-docs'; const config = new DocumentBuilder() @@ -30,7 +30,7 @@ function setupSwagger(app) { .build(); const document = SwaggerModule.createDocument(app, config, { - include: [InfraTokenModule], + include: isProduction ? [InfraTokenModule] : [], }); SwaggerModule.setup(swaggerDocPath, app, document, { swaggerOptions: { persistAuthorization: true, ignoreGlobalPrefix: true }, @@ -41,15 +41,11 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); + const isProduction = configService.get('PRODUCTION') === 'true'; - console.log(`Running in production: ${configService.get('PRODUCTION')}`); + console.log(`Running in production: ${isProduction}`); console.log(`Port: ${configService.get('PORT')}`); - checkEnvironmentAuthProvider( - configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS') ?? - configService.get('VITE_ALLOWED_AUTH_PROVIDERS'), - ); - app.use( session({ secret: @@ -72,7 +68,7 @@ async function bootstrap() { }), ); - if (configService.get('PRODUCTION') === 'true') { + if (isProduction) { console.log('Enabling CORS with production settings'); app.enableCors({ origin: configService.get('WHITELISTED_ORIGINS').split(','), @@ -95,8 +91,9 @@ async function bootstrap() { transform: true, }), ); + app.use(morgan(':method :url :status - :response-time ms')); - await setupSwagger(app); + await setupSwagger(app, isProduction); await app.listen(configService.get('PORT') || 3170); diff --git a/packages/hoppscotch-backend/src/team/team.service.spec.ts b/packages/hoppscotch-backend/src/team/team.service.spec.ts index 96ac41fa..44954535 100644 --- a/packages/hoppscotch-backend/src/team/team.service.spec.ts +++ b/packages/hoppscotch-backend/src/team/team.service.spec.ts @@ -910,7 +910,7 @@ describe('deleteUserFromAllTeams', () => { const result = teamService.deleteUserFromAllTeams(dbTeamMember.userUid)(); - await expect(result).rejects.toThrowError(TEAM_ONLY_ONE_OWNER); + await expect(result).rejects.toThrow(TEAM_ONLY_ONE_OWNER); expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({ where: { userUid: dbTeamMember.userUid, @@ -932,7 +932,7 @@ describe('deleteUserFromAllTeams', () => { const result = teamService.deleteUserFromAllTeams(dbTeamMember.userUid); - await expect(result).rejects.toThrowError(TEAM_INVALID_ID_OR_USER); + await expect(result).rejects.toThrow(TEAM_INVALID_ID_OR_USER); expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({ where: { userUid: dbTeamMember.userUid, diff --git a/packages/hoppscotch-backend/src/types/InfraConfig.ts b/packages/hoppscotch-backend/src/types/InfraConfig.ts index 30da8336..6563ae63 100644 --- a/packages/hoppscotch-backend/src/types/InfraConfig.ts +++ b/packages/hoppscotch-backend/src/types/InfraConfig.ts @@ -1,4 +1,18 @@ export enum InfraConfigEnum { + ONBOARDING_COMPLETED = 'ONBOARDING_COMPLETED', + ONBOARDING_RECOVERY_TOKEN = 'ONBOARDING_RECOVERY_TOKEN', + + JWT_SECRET = 'JWT_SECRET', + SESSION_SECRET = 'SESSION_SECRET', + TOKEN_SALT_COMPLEXITY = 'TOKEN_SALT_COMPLEXITY', + MAGIC_LINK_TOKEN_VALIDITY = 'MAGIC_LINK_TOKEN_VALIDITY', + REFRESH_TOKEN_VALIDITY = 'REFRESH_TOKEN_VALIDITY', + ACCESS_TOKEN_VALIDITY = 'ACCESS_TOKEN_VALIDITY', + ALLOW_SECURE_COOKIES = 'ALLOW_SECURE_COOKIES', + + RATE_LIMIT_TTL = 'RATE_LIMIT_TTL', + RATE_LIMIT_MAX = 'RATE_LIMIT_MAX', + MAILER_SMTP_ENABLE = 'MAILER_SMTP_ENABLE', MAILER_USE_CUSTOM_CONFIGS = 'MAILER_USE_CUSTOM_CONFIGS', MAILER_SMTP_URL = 'MAILER_SMTP_URL', diff --git a/packages/hoppscotch-backend/src/user-settings/user-settings.service.spec.ts b/packages/hoppscotch-backend/src/user-settings/user-settings.service.spec.ts index 909f55a2..48d60da3 100644 --- a/packages/hoppscotch-backend/src/user-settings/user-settings.service.spec.ts +++ b/packages/hoppscotch-backend/src/user-settings/user-settings.service.spec.ts @@ -83,7 +83,7 @@ describe('UserSettingsService', () => { await userSettingsService.createUserSettings(user, settings.properties); - expect(mockPubSub.publish).toBeCalledWith( + expect(mockPubSub.publish).toHaveBeenCalledWith( `user_settings/${user.uid}/created`, settings, ); @@ -126,7 +126,7 @@ describe('UserSettingsService', () => { await userSettingsService.updateUserSettings(user, settings.properties); - expect(mockPubSub.publish).toBeCalledWith( + expect(mockPubSub.publish).toHaveBeenCalledWith( `user_settings/${user.uid}/updated`, settings, ); diff --git a/packages/hoppscotch-backend/src/utils.ts b/packages/hoppscotch-backend/src/utils.ts index 69844e86..cb3c3c09 100644 --- a/packages/hoppscotch-backend/src/utils.ts +++ b/packages/hoppscotch-backend/src/utils.ts @@ -8,14 +8,7 @@ import { pipe } from 'fp-ts/lib/function'; import * as O from 'fp-ts/Option'; import * as T from 'fp-ts/Task'; import * as TE from 'fp-ts/TaskEither'; -import { AuthProvider } from './auth/helper'; -import { - ENV_EMPTY_AUTH_PROVIDERS, - ENV_NOT_FOUND_KEY_AUTH_PROVIDERS, - ENV_NOT_FOUND_KEY_DATA_ENCRYPTION_KEY, - ENV_NOT_SUPPORT_AUTH_PROVIDERS, - JSON_INVALID, -} from './errors'; +import { ENV_NOT_FOUND_KEY_DATA_ENCRYPTION_KEY, JSON_INVALID } from './errors'; import { TeamAccessRole } from './team/team.model'; import { RESTError } from './types/RESTError'; import * as crypto from 'crypto'; @@ -234,36 +227,6 @@ export function isValidLength(title: string, length: number) { return true; } -/** - * This function is called by bootstrap() in main.ts - * It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not. - * If not, it throws an error. - */ -export function checkEnvironmentAuthProvider( - VITE_ALLOWED_AUTH_PROVIDERS: string, -) { - if (!VITE_ALLOWED_AUTH_PROVIDERS) { - throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS); - } - - if (VITE_ALLOWED_AUTH_PROVIDERS === '') { - throw new Error(ENV_EMPTY_AUTH_PROVIDERS); - } - - const givenAuthProviders = VITE_ALLOWED_AUTH_PROVIDERS.split(',').map( - (provider) => provider.toLocaleUpperCase(), - ); - const supportedAuthProviders = Object.values(AuthProvider).map( - (provider: string) => provider.toLocaleUpperCase(), - ); - - for (const givenAuthProvider of givenAuthProviders) { - if (!supportedAuthProviders.includes(givenAuthProvider)) { - throw new Error(ENV_NOT_SUPPORT_AUTH_PROVIDERS); - } - } -} - /** * Adds escape backslashes to the input so that it can be used inside * SQL LIKE/ILIKE queries. Inspired by PHP's `mysql_real_escape_string` @@ -342,7 +305,7 @@ const ENCRYPTION_ALGORITHM = 'aes-256-cbc'; export function encrypt(text: string, key = process.env.DATA_ENCRYPTION_KEY) { if (!key) throw new Error(ENV_NOT_FOUND_KEY_DATA_ENCRYPTION_KEY); - if (text === null || text === undefined) return text; + if (!text || text === '') return text; const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv( @@ -367,7 +330,7 @@ export function decrypt( ) { if (!key) throw new Error(ENV_NOT_FOUND_KEY_DATA_ENCRYPTION_KEY); - if (encryptedData === null || encryptedData === undefined) { + if (!encryptedData || encryptedData === '') { return encryptedData; } diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index 7be9ddee..ff27fdec 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/packages/hoppscotch-common/locales/en.json @@ -1332,7 +1332,7 @@ "admin_success": "User is now an admin!!", "and": "and", "clear_selection": "Clear Selection", - "configure_auth": "Check out the documentation to configure auth providers.", + "configure_auth": "Please set up an auth provider from the admin settings or check out the documentation to configure auth providers.", "confirm_admin_to_user": "Do you want to remove admin status from this user?", "confirm_admins_to_users": "Do you want to remove admin status from selected users?", "confirm_delete_infra_token": "Are you sure you want to delete the infra token {tokenLabel}?", diff --git a/packages/hoppscotch-common/src/components/firebase/Login.vue b/packages/hoppscotch-common/src/components/firebase/Login.vue index c3a4bd12..e955b98e 100644 --- a/packages/hoppscotch-common/src/components/firebase/Login.vue +++ b/packages/hoppscotch-common/src/components/firebase/Login.vue @@ -56,6 +56,27 @@ :label="`${t('auth.send_magic_link')}`" /> + +
+

{{ t("state.require_auth_provider") }}

+

{{ t("state.configure_auth") }}

+
+ + + +
+
@@ -133,6 +154,7 @@ import IconGithub from "~icons/auth/github" import IconGoogle from "~icons/auth/google" import IconMicrosoft from "~icons/auth/microsoft" import IconArrowLeft from "~icons/lucide/arrow-left" +import IconFileText from "~icons/lucide/file-text" import { useService } from "dioc/vue" import { LoginItemDef } from "~/platform/auth" diff --git a/packages/hoppscotch-sh-admin/locales/en.json b/packages/hoppscotch-sh-admin/locales/en.json index 9201c670..3a0b5693 100644 --- a/packages/hoppscotch-sh-admin/locales/en.json +++ b/packages/hoppscotch-sh-admin/locales/en.json @@ -42,9 +42,21 @@ "sso": "SSO", "tenant": "TENANT", "title": "OAuth Providers", + "token": { + "description": "Configure token for your server", + "title": "Token", + "jwt_secret": " JWT Secret", + "token_salt_complexity": "Token Salt Complexity", + "magic_link_token_validity": "Magic Link Token Validity (in hour)", + "refresh_token_validity": "Refresh Token Validity (in milliseconds)", + "access_token_validity": "Access Token Validity (in milliseconds)", + "session_secret": "Session Secret", + "update_failure": "Failed to update token configurations!!" + }, "update_failure": "Failed to update authentication provider configurations!!" }, "confirm_changes": "Hoppscotch server must restart to reflect the new changes. Confirm changes made to the server configurations?", + "invalid_number": "Please enter a valid number for the field", "input_empty": "Please fill all the fields before updating the configurations", "input_validation_error": "Some fields have invalid values. Please correct them before updating the configurations", "data_sharing": { @@ -85,6 +97,14 @@ "toggle_failure": "Failed to toggle history!!", "clear_confirm": "Are you sure you want to clear all history?" }, + "rate_limit": { + "description": "Configure rate limiting for your Hoppscotch instance", + "rate_limit_max": "Maximum Requests", + "rate_limit_ttl": "Time to Leave (in seconds)", + "title": "Rate Limit Configurations", + "update_failure": "Failed to update rate limit configurations!!", + "input_validation_error": "Please enter valid values for rate limit configurations" + }, "reset": { "confirm_reset": "Hoppscotch server must restart to reflect the new changes. Confirm the reset of server configurations?", "description": "Default configurations will be loaded as specified in the environment file", @@ -102,6 +122,7 @@ "auth": "Authentication", "activity": "Activity", "infra_tokens": "Infra Tokens", + "rate_limit": "Rate Limit", "smtp": "SMTP", "miscellaneous": "Miscellaneous", "reset": "Reset Configurations" diff --git a/packages/hoppscotch-sh-admin/src/components/app/Login.vue b/packages/hoppscotch-sh-admin/src/components/app/Login.vue index 4510aeed..1017d355 100644 --- a/packages/hoppscotch-sh-admin/src/components/app/Login.vue +++ b/packages/hoppscotch-sh-admin/src/components/app/Login.vue @@ -17,7 +17,7 @@
-
+

{{ t('state.require_auth_provider') }}

{{ t('state.configure_auth') }}

@@ -102,7 +105,16 @@
-
+
+ Need to re-configure auth providers? + +
+ +
+import { computedAsync } from '@vueuse/core'; import { onMounted, ref } from 'vue'; import { useI18n } from '~/composables/i18n'; import { useToast } from '~/composables/toast'; @@ -183,6 +196,15 @@ const nonAdminUser = ref(false); const allowedAuthProviders = ref([]); +// check if the user can re-run onboarding +const canReRunOnboarding = computedAsync(async () => { + const onboardingStatus = await auth.getOnboardingStatus(); + + return ( + onboardingStatus?.onboardingCompleted && onboardingStatus.canReRunOnboarding + ); +}); + onMounted(async () => { const user = auth.getCurrentUser(); if (user && !user.isAdmin) { diff --git a/packages/hoppscotch-sh-admin/src/components/onboarding/AuthSetup.vue b/packages/hoppscotch-sh-admin/src/components/onboarding/AuthSetup.vue new file mode 100644 index 00000000..232c797c --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/components/onboarding/AuthSetup.vue @@ -0,0 +1,304 @@ + + + diff --git a/packages/hoppscotch-sh-admin/src/components/onboarding/CompleteScreen.vue b/packages/hoppscotch-sh-admin/src/components/onboarding/CompleteScreen.vue new file mode 100644 index 00000000..fdd3bd2e --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/components/onboarding/CompleteScreen.vue @@ -0,0 +1,112 @@ + + + diff --git a/packages/hoppscotch-sh-admin/src/components/onboarding/OAuthSetup.vue b/packages/hoppscotch-sh-admin/src/components/onboarding/OAuthSetup.vue new file mode 100644 index 00000000..82ed1dd6 --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/components/onboarding/OAuthSetup.vue @@ -0,0 +1,102 @@ + + + diff --git a/packages/hoppscotch-sh-admin/src/components/onboarding/SmtpSetup.vue b/packages/hoppscotch-sh-admin/src/components/onboarding/SmtpSetup.vue new file mode 100644 index 00000000..6a46da93 --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/components/onboarding/SmtpSetup.vue @@ -0,0 +1,160 @@ + + + diff --git a/packages/hoppscotch-sh-admin/src/components/onboarding/WelcomeScreen.vue b/packages/hoppscotch-sh-admin/src/components/onboarding/WelcomeScreen.vue new file mode 100644 index 00000000..76f65bb7 --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/components/onboarding/WelcomeScreen.vue @@ -0,0 +1,18 @@ + diff --git a/packages/hoppscotch-sh-admin/src/components/settings/AuthConfigurations.vue b/packages/hoppscotch-sh-admin/src/components/settings/AuthConfigurations.vue index 2eaa15c1..6ba2318f 100644 --- a/packages/hoppscotch-sh-admin/src/components/settings/AuthConfigurations.vue +++ b/packages/hoppscotch-sh-admin/src/components/settings/AuthConfigurations.vue @@ -61,6 +61,12 @@
+ + +
+ +
+
@@ -82,7 +88,7 @@ const emit = defineEmits<{ }>(); // Auth Sub Tabs -type AuthSubTabs = 'auth-providers' | 'email-auth'; +type AuthSubTabs = 'auth-providers' | 'email-auth' | 'token'; const selectedAuthSubTab = ref('auth-providers'); const workingConfigs = useVModel(props, 'config', emit); diff --git a/packages/hoppscotch-sh-admin/src/components/settings/AuthToken.vue b/packages/hoppscotch-sh-admin/src/components/settings/AuthToken.vue new file mode 100644 index 00000000..8fd40fb4 --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/components/settings/AuthToken.vue @@ -0,0 +1,205 @@ + + + + + diff --git a/packages/hoppscotch-sh-admin/src/components/settings/OAuthProviderConfigurations.vue b/packages/hoppscotch-sh-admin/src/components/settings/OAuthProviderConfigurations.vue index e1f0a797..aa11a984 100644 --- a/packages/hoppscotch-sh-admin/src/components/settings/OAuthProviderConfigurations.vue +++ b/packages/hoppscotch-sh-admin/src/components/settings/OAuthProviderConfigurations.vue @@ -43,7 +43,7 @@
@@ -75,6 +81,7 @@ import { useVModel } from '@vueuse/core'; import { reactive } from 'vue'; import { useI18n } from '~/composables/i18n'; import { ServerConfigs, SsoAuthProviders } from '~/helpers/configs'; +import { makeReadableKey } from '~/helpers/utils/readableKey'; import IconCircleHelp from '~icons/lucide/circle-help'; import IconEye from '~icons/lucide/eye'; import IconEyeOff from '~icons/lucide/eye-off'; diff --git a/packages/hoppscotch-sh-admin/src/components/settings/RateLimit.vue b/packages/hoppscotch-sh-admin/src/components/settings/RateLimit.vue new file mode 100644 index 00000000..da45805b --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/components/settings/RateLimit.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/packages/hoppscotch-sh-admin/src/components/settings/ServerRestart.vue b/packages/hoppscotch-sh-admin/src/components/settings/ServerRestart.vue index 100cd836..b12fff15 100644 --- a/packages/hoppscotch-sh-admin/src/components/settings/ServerRestart.vue +++ b/packages/hoppscotch-sh-admin/src/components/settings/ServerRestart.vue @@ -68,6 +68,8 @@ const { updateDataSharingConfigs, toggleSMTPConfigs, toggleUserHistoryStore, + updateRateLimitConfigs, + updateAuthTokenConfigs, } = useConfigHandler(props.workingConfigs); // Call relevant mutations on component mount and initiate server restart @@ -134,6 +136,22 @@ onMounted(async () => { if (!userHistoryStoreResult) { return triggerComponentUnMount(); } + + const rateLimitResult = await updateRateLimitConfigs( + updateInfraConfigsMutation + ); + + if (!rateLimitResult) { + return triggerComponentUnMount(); + } + + const authTokenResult = await updateAuthTokenConfigs( + updateInfraConfigsMutation + ); + + if (!authTokenResult) { + return triggerComponentUnMount(); + } } restart.value = true; diff --git a/packages/hoppscotch-sh-admin/src/components/settings/SmtpConfiguration.vue b/packages/hoppscotch-sh-admin/src/components/settings/SmtpConfiguration.vue index dc6df0cf..704d512a 100644 --- a/packages/hoppscotch-sh-admin/src/components/settings/SmtpConfiguration.vue +++ b/packages/hoppscotch-sh-admin/src/components/settings/SmtpConfiguration.vue @@ -60,23 +60,27 @@ :on="Boolean(smtpConfigs.fields[field.key])" @change="toggleCheckbox(field)" > - {{ field.name }} + {{ makeReadableKey(field.name, true) }}
- + - + class="!my-2 !bg-primaryLight flex-1 border border-divider rounded" + input-styles="!border-0" + > + +
+
+
+ +
+ +
+ +
+
+ + + diff --git a/packages/hoppscotch-sh-admin/src/composables/useConfigHandler.ts b/packages/hoppscotch-sh-admin/src/composables/useConfigHandler.ts index d259f333..453c45dc 100644 --- a/packages/hoppscotch-sh-admin/src/composables/useConfigHandler.ts +++ b/packages/hoppscotch-sh-admin/src/composables/useConfigHandler.ts @@ -136,6 +136,25 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) { getFieldValue(InfraConfigEnum.MailerUseCustomConfigs) === 'true', }, }, + tokenConfigs: { + name: 'token', + fields: { + jwt_secret: getFieldValue(InfraConfigEnum.JwtSecret), + token_salt_complexity: getFieldValue( + InfraConfigEnum.TokenSaltComplexity + ), + magic_link_token_validity: getFieldValue( + InfraConfigEnum.MagicLinkTokenValidity + ), + refresh_token_validity: getFieldValue( + InfraConfigEnum.RefreshTokenValidity + ), + access_token_validity: getFieldValue( + InfraConfigEnum.AccessTokenValidity + ), + session_secret: getFieldValue(InfraConfigEnum.SessionSecret), + }, + }, dataSharingConfigs: { name: 'data_sharing', enabled: !!infraConfigs.value.find( @@ -150,6 +169,13 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) { config.value === 'ENABLE' ), }, + rateLimitConfigs: { + name: 'rate_limit', + fields: { + rate_limit_ttl: getFieldValue(InfraConfigEnum.RateLimitTtl), + rate_limit_max: getFieldValue(InfraConfigEnum.RateLimitMax), + }, + }, }; // Cloning the current configs to working configs @@ -165,18 +191,39 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) { Check if any of the config fields are empty */ const isFieldEmpty = (field: string | boolean) => { - if (typeof field === 'boolean') { + if (typeof field === 'boolean' || typeof field === 'number') { return false; } return field.trim() === ''; }; + /** + * Check if the field is not valid + * This is used to validate number fields, ensuring they are not NaN or less than or equal to zero. + * @param field Field value to validate + * @returns Boolean indicating if the field is valid + */ + const isFieldNotValid = (field: string | boolean) => { + if (typeof field === 'boolean') { + return false; + } + + const num = Number(field); + if (isNaN(num) && typeof field === 'string') { + return field.trim() === ''; + } + + return num <= 0; + }; + const AreAnyConfigFieldsEmpty = (config: ServerConfigs): boolean => { const sections: Array = [ config.providers.github, config.providers.google, config.providers.microsoft, config.mailConfigs, + config.rateLimitConfigs, + config.tokenConfigs, ]; const hasSectionWithEmptyFields = sections.some((section) => { @@ -197,6 +244,11 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) { ); } + // This section has no enabled property, so we check fields directly + // eg: tokenConfigs, rateLimitConfigs + if (!('enabled' in section)) + Object.values(section.fields).some(isFieldNotValid); + return ( section.enabled && Object.values(section.fields).some(isFieldEmpty) ); @@ -407,6 +459,118 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) { 'configs.user_history_store.toggle_failure' ); + const updateRateLimitConfigs = ( + updateRateLimitMutation: UseMutationResponse + ) => { + if (!updatedConfigs?.rateLimitConfigs) { + toast.error(t('configs.rate_limit.input_validation_error')); + return false; + } + + const rateLimitTtl = String( + updatedConfigs?.rateLimitConfigs.fields.rate_limit_ttl + ); + const rateLimitMax = String( + updatedConfigs?.rateLimitConfigs.fields.rate_limit_max + ); + + if (isFieldEmpty(rateLimitTtl) || isFieldEmpty(rateLimitMax)) { + toast.error(t('configs.rate_limit.input_validation_error')); + return false; + } + + const rateLimitConfigs: InfraConfigArgs[] = [ + { + name: InfraConfigEnum.RateLimitTtl, + value: String(rateLimitTtl), + }, + { + name: InfraConfigEnum.RateLimitMax, + value: String(rateLimitMax), + }, + ]; + + return executeMutation( + updateRateLimitMutation, + { + infraConfigs: rateLimitConfigs, + }, + 'configs.rate_limit.update_failure' + ); + }; + + const updateAuthTokenConfigs = ( + updateAuthTokenMutation: UseMutationResponse + ) => { + if (!updatedConfigs?.tokenConfigs) { + toast.error(t('configs.auth_providers.token.update_failure')); + return false; + } + + const jwtSecret = String(updatedConfigs?.tokenConfigs.fields.jwt_secret); + const tokenSaltComplexity = String( + updatedConfigs?.tokenConfigs.fields.token_salt_complexity + ); + const magicLinkTokenValidity = String( + updatedConfigs?.tokenConfigs.fields.magic_link_token_validity + ); + const refreshTokenValidity = String( + updatedConfigs?.tokenConfigs.fields.refresh_token_validity + ); + const accessTokenValidity = String( + updatedConfigs?.tokenConfigs.fields.access_token_validity + ); + const sessionSecret = String( + updatedConfigs?.tokenConfigs.fields.session_secret + ); + if ( + isFieldEmpty(jwtSecret) || + isFieldEmpty(tokenSaltComplexity) || + isFieldEmpty(magicLinkTokenValidity) || + isFieldEmpty(refreshTokenValidity) || + isFieldEmpty(accessTokenValidity) || + isFieldEmpty(sessionSecret) + ) { + toast.error(t('configs.auth_providers.token.update_failure')); + return false; + } + + const authTokenConfigs: InfraConfigArgs[] = [ + { + name: InfraConfigEnum.JwtSecret, + value: jwtSecret, + }, + { + name: InfraConfigEnum.TokenSaltComplexity, + value: tokenSaltComplexity, + }, + { + name: InfraConfigEnum.MagicLinkTokenValidity, + value: magicLinkTokenValidity, + }, + { + name: InfraConfigEnum.RefreshTokenValidity, + value: refreshTokenValidity, + }, + { + name: InfraConfigEnum.AccessTokenValidity, + value: accessTokenValidity, + }, + { + name: InfraConfigEnum.SessionSecret, + value: sessionSecret, + }, + ]; + + return executeMutation( + updateAuthTokenMutation, + { + infraConfigs: authTokenConfigs, + }, + 'configs.auth_providers.token.update_failure' + ); + }; + return { currentConfigs, workingConfigs, @@ -414,6 +578,8 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) { updateDataSharingConfigs, toggleSMTPConfigs, toggleUserHistoryStore, + updateRateLimitConfigs, + updateAuthTokenConfigs, updateInfraConfigs, resetInfraConfigs, fetchingInfraConfigs, diff --git a/packages/hoppscotch-sh-admin/src/composables/useOnboardingConfigHandler.ts b/packages/hoppscotch-sh-admin/src/composables/useOnboardingConfigHandler.ts new file mode 100644 index 00000000..0899296b --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/composables/useOnboardingConfigHandler.ts @@ -0,0 +1,376 @@ +import { onMounted, ref, watch } from 'vue'; +import { useI18n } from './i18n'; +import { useToast } from './toast'; +import { auth } from '~/helpers/auth'; +import { InfraConfigEnum } from '~/helpers/backend/graphql'; +import { getLocalConfig, setLocalConfig } from '~/helpers/localpersistence'; +import { makeReadableKey } from '~/helpers/utils/readableKey'; + +export type OAuthProvider = 'GOOGLE' | 'GITHUB' | 'MICROSOFT'; +export type EnabledConfig = OAuthProvider | 'MAILER' | 'EMAIL'; + +// common OAuth keys used across providers +type OAuthKeys = 'CLIENT_ID' | 'CLIENT_SECRET' | 'CALLBACK_URL' | 'SCOPE'; + +// Microsoft specific keys +type MicrosoftKeys = OAuthKeys | 'TENANT'; + +type OAuthConfig = { + [K in Keys as `${Prefix}_${K}`]: string; +}; + +// Mailer specific keys +export type MailerConfigKeys = + | 'SMTP_ENABLE' + | 'USE_CUSTOM_CONFIGS' + | 'SMTP_URL' + | 'ADDRESS_FROM' + | 'SMTP_HOST' + | 'SMTP_PORT' + | 'SMTP_SECURE' + | 'SMTP_USER' + | 'SMTP_PASSWORD' + | 'TLS_REJECT_UNAUTHORIZED'; + +export type Configs = { + oAuthProviders: { + GOOGLE: OAuthConfig; + GITHUB: OAuthConfig; + MICROSOFT: OAuthConfig; + }; + mailerConfigs: { + [K in `MAILER_${MailerConfigKeys}`]: string; + }; +}; + +export type OnBoardingSummary = { + type: 'success' | 'error'; + message: string; + description: string; + configsAdded: string[]; +}; + +function mapOAuthProviders( + configs: Partial> +): Configs['oAuthProviders'] { + return { + GOOGLE: { + GOOGLE_CLIENT_ID: configs.GOOGLE_CLIENT_ID ?? '', + GOOGLE_CLIENT_SECRET: configs.GOOGLE_CLIENT_SECRET ?? '', + GOOGLE_CALLBACK_URL: '', + GOOGLE_SCOPE: configs.GOOGLE_SCOPE ?? '', + }, + GITHUB: { + GITHUB_CLIENT_ID: configs.GITHUB_CLIENT_ID ?? '', + GITHUB_CLIENT_SECRET: configs.GITHUB_CLIENT_SECRET ?? '', + GITHUB_CALLBACK_URL: '', + GITHUB_SCOPE: configs.GITHUB_SCOPE ?? '', + }, + MICROSOFT: { + MICROSOFT_CLIENT_ID: configs.MICROSOFT_CLIENT_ID ?? '', + MICROSOFT_CLIENT_SECRET: configs.MICROSOFT_CLIENT_SECRET ?? '', + MICROSOFT_CALLBACK_URL: '', + MICROSOFT_SCOPE: configs.MICROSOFT_SCOPE ?? '', + MICROSOFT_TENANT: configs.MICROSOFT_TENANT ?? '', + }, + }; +} + +function mapMailerConfigs( + configs: Partial> +): Configs['mailerConfigs'] { + return { + MAILER_SMTP_ENABLE: configs.MAILER_SMTP_ENABLE ?? '', + MAILER_USE_CUSTOM_CONFIGS: configs.MAILER_USE_CUSTOM_CONFIGS || 'false', + MAILER_SMTP_URL: configs.MAILER_SMTP_URL ?? '', + MAILER_ADDRESS_FROM: configs.MAILER_ADDRESS_FROM ?? '', + MAILER_SMTP_HOST: configs.MAILER_SMTP_HOST ?? '', + MAILER_SMTP_PORT: configs.MAILER_SMTP_PORT ?? '', + MAILER_SMTP_SECURE: configs.MAILER_SMTP_SECURE || 'false', + MAILER_SMTP_USER: configs.MAILER_SMTP_USER ?? '', + MAILER_SMTP_PASSWORD: configs.MAILER_SMTP_PASSWORD ?? '', + MAILER_TLS_REJECT_UNAUTHORIZED: + configs.MAILER_TLS_REJECT_UNAUTHORIZED || 'false', + }; +} + +/** + * The handler for onboarding configuration. + * This composable manages the state and logic for onboarding configurations, + * including enabling/disabling configs, validating inputs, + * and submitting the onboarding form. + * @returns Composable for handling onboarding configuration. + */ +export function useOnboardingConfigHandler() { + const t = useI18n(); + const toast = useToast(); + + const enabledConfigs = ref([]); + const isProvidersLoading = ref(false); + const submittingConfigs = ref(false); + + const onBoardingSummary = ref({ + type: 'success', + message: t('onboarding.addConfigsSuccess'), + description: t('onboarding.addConfigsDescription'), + configsAdded: [] as string[], + }); + + const currentConfigs = ref({ + oAuthProviders: mapOAuthProviders({}), + mailerConfigs: mapMailerConfigs({}), + }); + + const enableConfig = (config: EnabledConfig) => { + if (!enabledConfigs.value.includes(config)) { + enabledConfigs.value.push(config); + } + }; + + const toggleConfig = (key: EnabledConfig | 'OAUTH' | 'EMAIL') => { + if (key === 'OAUTH') { + enabledConfigs.value = enabledConfigs.value.filter( + (c) => !['GOOGLE', 'GITHUB', 'MICROSOFT'].includes(c) + ); + return; + } + + if (key === 'EMAIL') { + const hasEmail = enabledConfigs.value.includes('EMAIL'); + const hasMailer = enabledConfigs.value.includes('MAILER'); + enabledConfigs.value = enabledConfigs.value.filter( + (c) => c !== 'EMAIL' && c !== 'MAILER' + ); + if (!hasEmail || !hasMailer) { + enabledConfigs.value.push('EMAIL', 'MAILER'); + } + return; + } + + if (enabledConfigs.value.includes(key)) { + enabledConfigs.value = enabledConfigs.value.filter((c) => c !== key); + } else { + enableConfig(key); + } + }; + + const toggleSmtpConfig = () => { + const current = currentConfigs.value.mailerConfigs.MAILER_SMTP_ENABLE; + currentConfigs.value.mailerConfigs.MAILER_SMTP_ENABLE = + current === 'true' ? 'false' : 'true'; + }; + + // Set callback URLs for OAuth providers based on the current backend API URL + const setCallbackUrls = () => { + const base = import.meta.env.VITE_BACKEND_API_URL; + const oAuth = currentConfigs.value.oAuthProviders; + + if (oAuth.GOOGLE.GOOGLE_CLIENT_ID) { + oAuth.GOOGLE.GOOGLE_CALLBACK_URL = `${base}/auth/google/callback`; + } + if (oAuth.GITHUB.GITHUB_CLIENT_ID) { + oAuth.GITHUB.GITHUB_CALLBACK_URL = `${base}/auth/github/callback`; + } + if (oAuth.MICROSOFT.MICROSOFT_CLIENT_ID) { + oAuth.MICROSOFT.MICROSOFT_CALLBACK_URL = `${base}/auth/microsoft/callback`; + } + }; + + const makeOnboardingSummary = (error?: Error): OnBoardingSummary => { + const addedConfigs = enabledConfigs.value; + + if (addedConfigs.length === 0) { + return { + type: 'error', + message: t('onboarding.addConfigsError'), + description: t('onboarding.addConfigsDescription', { + error: error?.message || t('onboarding.addConfigsDefaultError'), + }), + configsAdded: [], + }; + } + + return { + type: 'success', + message: t('onboarding.addConfigsSuccess'), + description: t('onboarding.addConfigsDescription'), + configsAdded: addedConfigs.filter((key) => key !== 'MAILER'), + }; + }; + + /** + * Filters out unnecessary configs based on the current state. + * For example, if MAILER_USE_CUSTOM_CONFIGS is false, + * we don't need MAILER_SMTP_URL, MAILER_TLS_REJECT_UNAUTHORIZED, MAILER_SMTP_SECURE. + * @param keys Array of config keys to filter + * @returns Filtered array of keys that are needed based on the current state + */ + const filterNeededConfigs = (keys: string[]) => { + const mailer = currentConfigs.value.mailerConfigs; + const usingCustom = mailer.MAILER_USE_CUSTOM_CONFIGS === 'true'; + + return keys.filter((key) => { + if (!key.startsWith('MAILER_')) return true; + if (!enabledConfigs.value.includes('MAILER')) return false; + + if (!usingCustom) { + return ['MAILER_SMTP_URL', 'MAILER_ADDRESS_FROM'].includes(key); + } + + return [ + 'MAILER_SMTP_HOST', + 'MAILER_SMTP_PORT', + 'MAILER_SMTP_USER', + 'MAILER_SMTP_PASSWORD', + ].includes(key); + }); + }; + + /** + * Validates the provided configs. + * Checks if all required fields are filled and returns a filtered object + * with only the enabled configs that have values. + * @param configs Object containing config key-value pairs + * @returns Filtered object with valid configs or undefined if validation fails + */ + const validateConfigs = (configs: Partial>) => { + if (!configs || Object.keys(configs).length === 0) { + toast.error(t('onboarding.addConfigsError')); + return; + } + + const relevantKeys = Object.keys(configs).filter((key) => + enabledConfigs.value.includes(key.split('_')[0] as EnabledConfig) + ); + + const neededKeys = filterNeededConfigs(relevantKeys); + const allFilled = neededKeys.every((key) => configs[key]); + + if (!allFilled) { + neededKeys.forEach((key) => { + if (!configs[key]) + toast.error( + `Please fill the required field: ${makeReadableKey(key)}` + ); + }); + return; + } + + return Object.fromEntries( + Object.entries(configs).filter( + ([key, val]) => + enabledConfigs.value.includes(key.split('_')[0] as EnabledConfig) && + val + ) + ); + }; + + /** + * Adds the onboarding configs to the backend. + * It validates the configs, prepares the payload, + * and sends it to the backend API. + * We set the token in localStorage for re-fetching configs later. + * @returns The token for re-fetching configs or undefined if failed + */ + const addOnBoardingConfigs = async () => { + submittingConfigs.value = true; + const payload = { + ...currentConfigs.value.oAuthProviders.GOOGLE, + ...currentConfigs.value.oAuthProviders.GITHUB, + ...currentConfigs.value.oAuthProviders.MICROSOFT, + ...currentConfigs.value.mailerConfigs, + }; + + const validated = validateConfigs(payload); + + if (!validated || Object.keys(validated).length === 0) { + toast.error('Please add at least one config'); + return; + } + + const configWithAuth = { + ...validated, + [InfraConfigEnum.ViteAllowedAuthProviders]: enabledConfigs.value + .filter((x) => x !== 'MAILER') + .join(','), + }; + + try { + const res = await auth.addOnBoardingConfigs(configWithAuth); + if (res?.token) { + setLocalConfig('access_token', res.token); + toast.success('Onboarding configs added successfully'); + onBoardingSummary.value = makeOnboardingSummary(); + return res; + } + } catch (err) { + console.error('Failed to add onboarding configs', err); + toast.error('Failed to add onboarding configs'); + onBoardingSummary.value = makeOnboardingSummary(err as Error); + } finally { + submittingConfigs.value = false; + } + }; + + // Fetch onboarding configs on mount and populate the currentConfigs + // and enabledConfigs based on the response. + // This is used to pre-fill the form with existing configs. + onMounted(async () => { + try { + isProvidersLoading.value = true; + const token = getLocalConfig('access_token'); + if (!token) return; + + const configs = await auth.getOnboardingConfigs(token); + if (!configs) return; + + const allowed = configs[InfraConfigEnum.ViteAllowedAuthProviders]; + if (allowed) { + enabledConfigs.value = allowed.split(',') as EnabledConfig[]; + } + + currentConfigs.value = { + oAuthProviders: mapOAuthProviders(configs), + mailerConfigs: mapMailerConfigs(configs), + }; + } catch (err) { + console.error('Error fetching onboarding configs', err); + } finally { + isProvidersLoading.value = false; + } + }); + + // Watch for changes in currentConfigs and update the callback URLs + // and enable/disable configs based on the SMTP settings. + // This ensures that the form reflects the current state of the configs. + watch( + currentConfigs, + () => { + setCallbackUrls(); + + if ( + currentConfigs.value.mailerConfigs.MAILER_SMTP_ENABLE?.toLowerCase() === + 'true' + ) { + // Enable MAILER and EMAIL configs if SMTP is enabled + // because we need to add EMAIL in VITE_ALLOWED_AUTH_PROVIDERS + // and MAILER because the key is used in backend + enableConfig('MAILER'); + enableConfig('EMAIL'); + } + }, + { deep: true, immediate: true } + ); + + return { + currentConfigs, + enabledConfigs, + isProvidersLoading, + onBoardingSummary, + submittingConfigs, + toggleConfig, + toggleSmtpConfig, + enabledConfig: enableConfig, + addOnBoardingConfigs, + }; +} diff --git a/packages/hoppscotch-sh-admin/src/helpers/auth.ts b/packages/hoppscotch-sh-admin/src/helpers/auth.ts index b1ea2c69..1002d666 100644 --- a/packages/hoppscotch-sh-admin/src/helpers/auth.ts +++ b/packages/hoppscotch-sh-admin/src/helpers/auth.ts @@ -48,6 +48,11 @@ export const authEvents$ = new Subject< AuthEvent | { event: 'token_refresh' } >(); +export type OnboardingStatus = { + canReRunOnboarding: boolean; + onboardingCompleted: boolean; +}; + const currentUser$ = new BehaviorSubject(null); const signOut = async (reloadWindow = false) => { @@ -252,4 +257,36 @@ export const auth = { return false; } }, + + getOnboardingStatus: async (): Promise => { + try { + const res = await authQuery.getOnboardingStatus(); + return res.data; + } catch (err) { + console.error(err); + return null; + } + }, + + addOnBoardingConfigs: async (config: Record) => { + try { + const res = await authQuery.addOnBoardingConfigs(config); + return res.data as { + token: string; + }; + } catch (err) { + console.error(err); + return null; + } + }, + + getOnboardingConfigs: async (token: string) => { + try { + const res = await authQuery.getOnBoardingConfigs(token); + return res.data; + } catch (err) { + console.error(err); + return null; + } + }, }; diff --git a/packages/hoppscotch-sh-admin/src/helpers/backend/rest/authQuery.ts b/packages/hoppscotch-sh-admin/src/helpers/backend/rest/authQuery.ts index 5ab3023b..40f573e4 100644 --- a/packages/hoppscotch-sh-admin/src/helpers/backend/rest/authQuery.ts +++ b/packages/hoppscotch-sh-admin/src/helpers/backend/rest/authQuery.ts @@ -31,5 +31,10 @@ export default { }), getFirstTimeInfraSetupStatus: () => restApi.get('/site/setup'), updateFirstTimeInfraSetupStatus: () => restApi.put('/site/setup'), + addOnBoardingConfigs: (config: Record) => + restApi.post('/onboarding/config', config), + getOnboardingStatus: () => restApi.get('/onboarding/status'), + getOnBoardingConfigs: (token: string) => + restApi.get('/onboarding/config?token=' + token), logout: () => restApi.get('/auth/logout'), }; diff --git a/packages/hoppscotch-sh-admin/src/helpers/configs.ts b/packages/hoppscotch-sh-admin/src/helpers/configs.ts index 2a6f8d81..9dcaf118 100644 --- a/packages/hoppscotch-sh-admin/src/helpers/configs.ts +++ b/packages/hoppscotch-sh-admin/src/helpers/configs.ts @@ -58,6 +58,18 @@ export type ServerConfigs = { }; }; + tokenConfigs: { + name: string; + fields: { + jwt_secret: string; + token_salt_complexity: string; + magic_link_token_validity: string; + refresh_token_validity: string; + access_token_validity: string; + session_secret: string; + }; + }; + historyConfig: { name: string; enabled: boolean; @@ -67,6 +79,14 @@ export type ServerConfigs = { name: string; enabled: boolean; }; + + rateLimitConfigs: { + name: string; + fields: { + rate_limit_ttl: string; + rate_limit_max: string; + }; + }; }; export type UpdatedConfigs = { @@ -82,7 +102,7 @@ export type ConfigTransform = { export type ConfigSection = { name: SsoAuthProviders | string; - enabled: boolean; + enabled?: boolean; fields: Record; }; @@ -211,6 +231,44 @@ export const HISTORY_STORE_CONFIG: Config[] = [ }, ]; +export const RATE_LIMIT_CONFIGS: Config[] = [ + { + name: InfraConfigEnum.RateLimitTtl, + key: 'rate_limit_ttl', + }, + { + name: InfraConfigEnum.RateLimitMax, + key: 'rate_limit_max', + }, +]; + +export const TOKEN_VALIDATION_CONFIGS: Config[] = [ + { + name: InfraConfigEnum.JwtSecret, + key: 'jwt_secret', + }, + { + name: InfraConfigEnum.SessionSecret, + key: 'session_secret', + }, + { + name: InfraConfigEnum.TokenSaltComplexity, + key: 'token_salt_complexity', + }, + { + name: InfraConfigEnum.MagicLinkTokenValidity, + key: 'magic_link_token_validity', + }, + { + name: InfraConfigEnum.RefreshTokenValidity, + key: 'refresh_token_validity', + }, + { + name: InfraConfigEnum.AccessTokenValidity, + key: 'access_token_validity', + }, +]; + export const ALL_CONFIGS = [ GOOGLE_CONFIGS, MICROSOFT_CONFIGS, @@ -219,4 +277,6 @@ export const ALL_CONFIGS = [ CUSTOM_MAIL_CONFIGS, DATA_SHARING_CONFIGS, HISTORY_STORE_CONFIG, + RATE_LIMIT_CONFIGS, + TOKEN_VALIDATION_CONFIGS, ]; diff --git a/packages/hoppscotch-sh-admin/src/helpers/utils/readableKey.ts b/packages/hoppscotch-sh-admin/src/helpers/utils/readableKey.ts new file mode 100644 index 00000000..6f7cc311 --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/helpers/utils/readableKey.ts @@ -0,0 +1,20 @@ +/** + * The makeReadableKey function formats a string to be more human-readable. + * It replaces underscores with spaces, converts it to lowercase, + * and capitalizes the first letter. + * @param string The string to be formatted + * @returns A human-readable version of the string + */ +export const makeReadableKey = (string: string, capitalizeAll?: boolean) => { + if (!string) return ''; + const val = string.replace(/_/g, ' ').toLocaleLowerCase(); + + if (capitalizeAll) { + return val + .split(' ') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } + + return String(val).charAt(0).toUpperCase() + String(val).slice(1); +}; diff --git a/packages/hoppscotch-sh-admin/src/modules/admin.ts b/packages/hoppscotch-sh-admin/src/modules/admin.ts index 96c06dbd..fd266904 100644 --- a/packages/hoppscotch-sh-admin/src/modules/admin.ts +++ b/packages/hoppscotch-sh-admin/src/modules/admin.ts @@ -4,7 +4,8 @@ import { HoppModule } from '.'; const isSetupRoute = (to: unknown) => to === 'setup'; -const isGuestRoute = (to: unknown) => ['index', 'enter'].includes(to as string); +const isGuestRoute = (to: unknown) => + ['index', 'enter', 'onboarding'].includes(to as string); const getFirstTimeInfraSetupStatus = async () => { const isInfraNotSetup = await auth.getFirstTimeInfraSetupStatus(); @@ -23,9 +24,20 @@ const getFirstTimeInfraSetupStatus = async () => { * @param {function} next * @returns {void} */ - export default { async onBeforeRouteChange(to, _from, next) { + // Check if onboarding is completed + const onboardingStatus = await auth.getOnboardingStatus(); + + if ( + !onboardingStatus?.onboardingCompleted && + to.name !== 'onboarding' && + to.name === 'index' + ) { + // If onboarding is not completed, redirect to the onboarding page + return next({ name: 'onboarding' }); + } + const res = await auth.getUserDetails(); // Allow performing the silent refresh flow for an invalid access token state @@ -35,6 +47,15 @@ export default { const isAdmin = res.data?.me.isAdmin; + if ( + onboardingStatus?.onboardingCompleted && + !onboardingStatus.canReRunOnboarding && + to.name === 'onboarding' + ) { + // If onboarding is completed, redirect to the dashboard + return next({ name: 'index' }); + } + // Route Guards if (!isGuestRoute(to.name) && !isAdmin) { /** diff --git a/packages/hoppscotch-sh-admin/src/pages/onboarding.vue b/packages/hoppscotch-sh-admin/src/pages/onboarding.vue new file mode 100644 index 00000000..81de8de3 --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/pages/onboarding.vue @@ -0,0 +1,135 @@ + + + + +meta: + layout: empty + diff --git a/packages/hoppscotch-sh-admin/src/pages/settings.vue b/packages/hoppscotch-sh-admin/src/pages/settings.vue index bfe1431a..d8ef68c1 100644 --- a/packages/hoppscotch-sh-admin/src/pages/settings.vue +++ b/packages/hoppscotch-sh-admin/src/pages/settings.vue @@ -31,6 +31,9 @@ + + +
@@ -76,7 +79,7 @@ const showSaveChangesModal = ref(false); const initiateServerRestart = ref(false); // Tabs -type OptionTabs = 'auth' | 'smtp' | 'token' | 'miscellaneous'; +type OptionTabs = 'auth' | 'smtp' | 'token' | 'miscellaneous' | 'rate-limit'; const selectedOptionTab = ref('auth'); // Obtain the current and working configs from the useConfigHandler composable diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ef6c7d8..5bf1997f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,7 +156,7 @@ importers: version: 4.12.1(graphql@16.11.0) '@nestjs-modules/mailer': specifier: 2.0.2 - version: 2.0.2(@nestjs/common@11.1.5(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.5)(nodemailer@7.0.5)(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + version: 2.0.2(@nestjs/common@11.1.5(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.5)(nodemailer@7.0.5)(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) '@nestjs/apollo': specifier: 13.1.0 version: 13.1.0(@apollo/server@4.12.1(graphql@16.11.0))(@nestjs/common@11.1.5(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.5)(@nestjs/graphql@13.1.0(@nestjs/common@11.1.5(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.5)(class-transformer@0.5.1)(class-validator@0.14.2)(graphql@16.11.0)(reflect-metadata@0.2.2))(graphql@16.11.0) @@ -244,6 +244,9 @@ importers: luxon: specifier: 3.7.1 version: 3.7.1 + morgan: + specifier: 1.10.1 + version: 1.10.1 nodemailer: specifier: 7.0.5 version: 7.0.5 @@ -680,7 +683,7 @@ importers: version: socket.io-client@2.5.0 socket.io-client-v3: specifier: npm:socket.io-client@3.1.3 - version: socket.io-client@4.8.0 + version: socket.io-client@3.1.3 socket.io-client-v4: specifier: npm:socket.io-client@4.8.0 version: socket.io-client@4.8.0 @@ -1425,6 +1428,9 @@ importers: cross-env: specifier: 7.0.3 version: 7.0.3 + dotenv: + specifier: 17.2.1 + version: 17.2.1 eslint: specifier: 8.47.0 version: 8.47.0 @@ -1618,6 +1624,9 @@ importers: cross-env: specifier: 7.0.3 version: 7.0.3 + dotenv: + specifier: 17.2.1 + version: 17.2.1 eslint: specifier: 8.57.0 version: 8.57.0 @@ -1817,6 +1826,9 @@ importers: '@vue/compiler-sfc': specifier: 3.5.12 version: 3.5.12 + autoprefixer: + specifier: 10.4.21 + version: 10.4.21(postcss@8.4.47) dotenv: specifier: 16.4.5 version: 16.4.5 @@ -1990,6 +2002,7 @@ packages: '@apollo/server@4.12.1': resolution: {integrity: sha512-jceK1LK6l074in4mWu8XkwzDjvxdSzkmk8M7YpVLOGjQ6PmlaChcGRJ9BUwB46r22MxdPCXdC/6U5Vi8gzYbvQ==} engines: {node: '>=14.16.0'} + deprecated: Apollo Server v4 is deprecated and will transition to end-of-life on January 26, 2026. As long as you are already using a non-EOL version of Node.js, upgrading to v5 should take only a few minutes. See https://www.apollographql.com/docs/apollo-server/previous-versions for details. peerDependencies: graphql: ^16.6.0 @@ -6102,6 +6115,10 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@trysound/sax@0.2.0': + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -6138,6 +6155,9 @@ packages: '@types/caseless@0.12.5': resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + '@types/component-emitter@1.2.14': + resolution: {integrity: sha512-lmPil1g82wwWg/qHSxMWkSKyJGQOK+ejXeMAAWyxNtVUD0/Ycj2maL63RAqpxVfdtvTfZkRnqzB0A9ft59y69g==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -7459,6 +7479,13 @@ packages: peerDependencies: postcss: ^8.1.0 + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -7623,11 +7650,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - browserslist@4.25.1: - resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - bs-logger@0.2.6: resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} engines: {node: '>= 6'} @@ -7723,9 +7745,6 @@ packages: caniuse-lite@1.0.30001718: resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} - caniuse-lite@1.0.30001727: - resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} - canvas@2.11.2: resolution: {integrity: sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==} engines: {node: '>=6'} @@ -7934,10 +7953,6 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} - commander@11.1.0: - resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} - engines: {node: '>=16'} - commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -8174,8 +8189,8 @@ packages: resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} - css-tree@3.1.0: - resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} css-what@6.1.0: @@ -8190,23 +8205,23 @@ packages: cssfilter@0.0.10: resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==} - cssnano-preset-default@7.0.8: - resolution: {integrity: sha512-d+3R2qwrUV3g4LEMOjnndognKirBZISylDZAF/TPeCWVjEwlXS2e4eN4ICkoobRe7pD3H6lltinKVyS1AJhdjQ==} + cssnano-preset-default@7.0.6: + resolution: {integrity: sha512-ZzrgYupYxEvdGGuqL+JKOY70s7+saoNlHSCK/OGn1vB2pQK8KSET8jvenzItcY+kA7NoWvfbb/YhlzuzNKjOhQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - cssnano-utils@5.0.1: - resolution: {integrity: sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg==} + cssnano-utils@5.0.0: + resolution: {integrity: sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - cssnano@7.1.0: - resolution: {integrity: sha512-Pu3rlKkd0ZtlCUzBrKL1Z4YmhKppjC1H9jo7u1o4qaKqyhvixFgu5qLyNIAOjSTg9DjVPtUqdROq2EfpVMEe+w==} + cssnano@7.0.6: + resolution: {integrity: sha512-54woqx8SCbp8HwvNZYn68ZFAepuouZW4lTwiMVnBErM3VkO7/Sd4oTOt3Zz3bPx3kxQ36aISppyXj2Md4lg8bw==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 csso@5.0.5: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} @@ -8540,6 +8555,10 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} + dotenv@17.2.1: + resolution: {integrity: sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==} + engines: {node: '>=12'} + dset@3.1.4: resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} engines: {node: '>=4'} @@ -8571,9 +8590,6 @@ packages: electron-to-chromium@1.5.155: resolution: {integrity: sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==} - electron-to-chromium@1.5.191: - resolution: {integrity: sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==} - electron-to-chromium@1.5.35: resolution: {integrity: sha512-hOSRInrIDm0Brzp4IHW2F/VM+638qOL2CzE0DgpnGzKW27C95IqqeqgKz/hxHGnvPxvQGpHUGD5qRVC9EZY2+A==} @@ -8612,12 +8628,19 @@ packages: engine.io-client@3.5.4: resolution: {integrity: sha512-ydc8uuMMDxC5KCKNJN3zZKYJk2sgyTuTZQ7Aj1DJSsLKAcizA/PzWivw8fZMIjJVBo2CJOYzntv4FSjY/Lr//g==} + engine.io-client@4.1.4: + resolution: {integrity: sha512-843fqAdKeUMFqKi1sSjnR11tJ4wi8sIefu6+JC1OzkkJBmjtc/gM/rZ53tJfu5Iae/3gApm5veoS+v+gtT0+Fg==} + engine.io-client@6.6.1: resolution: {integrity: sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==} engine.io-parser@2.2.1: resolution: {integrity: sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==} + engine.io-parser@4.0.3: + resolution: {integrity: sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA==} + engines: {node: '>=8.0.0'} + engine.io-parser@5.2.3: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} @@ -8667,6 +8690,10 @@ packages: resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} engines: {node: '>= 0.4'} + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -8689,6 +8716,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} @@ -9324,6 +9355,14 @@ packages: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + form-data@4.0.4: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} @@ -9704,6 +9743,10 @@ packages: resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} engines: {node: '>= 0.4'} + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -9750,12 +9793,12 @@ packages: resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} engines: {node: '>=14'} - htmlnano@2.1.2: - resolution: {integrity: sha512-8Fst+0bhAfU362S6oHVb4wtJj/UYEFr0qiCLAEi8zioqmp1JYBQx5crZAADlFVX0Ly/6s/IQz6G7PL9/hgoJaQ==} + htmlnano@2.1.1: + resolution: {integrity: sha512-kAERyg/LuNZYmdqgCdYvugyLWNFAm8MWXpQMz1pLpetmCbFwoMxvkSoaAMlFrOC4OKTWI4KlZGT/RsNxg4ghOw==} peerDependencies: cssnano: ^7.0.0 postcss: ^8.3.11 - purgecss: ^7.0.2 + purgecss: ^6.0.0 relateurl: ^0.2.7 srcset: 5.0.1 svgo: ^3.0.2 @@ -10670,10 +10713,6 @@ packages: resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} engines: {node: '>=14'} - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} - lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -10933,8 +10972,8 @@ packages: mdn-data@2.0.28: resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} - mdn-data@2.12.2: - resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -11218,6 +11257,10 @@ packages: monaco-editor@0.52.2: resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + morgan@1.10.1: + resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} + engines: {node: '>= 0.8.0'} + mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -11461,6 +11504,10 @@ packages: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -11842,47 +11889,47 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss-calc@10.1.1: - resolution: {integrity: sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==} + postcss-calc@10.0.2: + resolution: {integrity: sha512-DT/Wwm6fCKgpYVI7ZEWuPJ4az8hiEHtCUeYjZXqU7Ou4QqYh1Df2yCQ7Ca6N7xqKPFkxN3fhf+u9KSoOCJNAjg==} engines: {node: ^18.12 || ^20.9 || >=22.0} peerDependencies: postcss: ^8.4.38 - postcss-colormin@7.0.4: - resolution: {integrity: sha512-ziQuVzQZBROpKpfeDwmrG+Vvlr0YWmY/ZAk99XD+mGEBuEojoFekL41NCsdhyNUtZI7DPOoIWIR7vQQK9xwluw==} + postcss-colormin@7.0.2: + resolution: {integrity: sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-convert-values@7.0.6: - resolution: {integrity: sha512-MD/eb39Mr60hvgrqpXsgbiqluawYg/8K4nKsqRsuDX9f+xN1j6awZCUv/5tLH8ak3vYp/EMXwdcnXvfZYiejCQ==} + postcss-convert-values@7.0.4: + resolution: {integrity: sha512-e2LSXPqEHVW6aoGbjV9RsSSNDO3A0rZLCBxN24zvxF25WknMPpX8Dm9UxxThyEbaytzggRuZxaGXqaOhxQ514Q==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-discard-comments@7.0.4: - resolution: {integrity: sha512-6tCUoql/ipWwKtVP/xYiFf1U9QgJ0PUvxN7pTcsQ8Ns3Fnwq1pU5D5s1MhT/XySeLq6GXNvn37U46Ded0TckWg==} + postcss-discard-comments@7.0.3: + resolution: {integrity: sha512-q6fjd4WU4afNhWOA2WltHgCbkRhZPgQe7cXF74fuVB/ge4QbM9HEaOIzGSiMvM+g/cOsNAUGdf2JDzqA2F8iLA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-discard-duplicates@7.0.2: - resolution: {integrity: sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w==} + postcss-discard-duplicates@7.0.1: + resolution: {integrity: sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-discard-empty@7.0.1: - resolution: {integrity: sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg==} + postcss-discard-empty@7.0.0: + resolution: {integrity: sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-discard-overridden@7.0.1: - resolution: {integrity: sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg==} + postcss-discard-overridden@7.0.0: + resolution: {integrity: sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} @@ -11926,41 +11973,41 @@ packages: yaml: optional: true - postcss-merge-longhand@7.0.5: - resolution: {integrity: sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw==} + postcss-merge-longhand@7.0.4: + resolution: {integrity: sha512-zer1KoZA54Q8RVHKOY5vMke0cCdNxMP3KBfDerjH/BYHh4nCIh+1Yy0t1pAEQF18ac/4z3OFclO+ZVH8azjR4A==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-merge-rules@7.0.6: - resolution: {integrity: sha512-2jIPT4Tzs8K87tvgCpSukRQ2jjd+hH6Bb8rEEOUDmmhOeTcqDg5fEFK8uKIu+Pvc3//sm3Uu6FRqfyv7YF7+BQ==} + postcss-merge-rules@7.0.4: + resolution: {integrity: sha512-ZsaamiMVu7uBYsIdGtKJ64PkcQt6Pcpep/uO90EpLS3dxJi6OXamIobTYcImyXGoW0Wpugh7DSD3XzxZS9JCPg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-minify-font-values@7.0.1: - resolution: {integrity: sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ==} + postcss-minify-font-values@7.0.0: + resolution: {integrity: sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-minify-gradients@7.0.1: - resolution: {integrity: sha512-X9JjaysZJwlqNkJbUDgOclyG3jZEpAMOfof6PUZjPnPrePnPG62pS17CjdM32uT1Uq1jFvNSff9l7kNbmMSL2A==} + postcss-minify-gradients@7.0.0: + resolution: {integrity: sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-minify-params@7.0.4: - resolution: {integrity: sha512-3OqqUddfH8c2e7M35W6zIwv7jssM/3miF9cbCSb1iJiWvtguQjlxZGIHK9JRmc8XAKmE2PFGtHSM7g/VcW97sw==} + postcss-minify-params@7.0.2: + resolution: {integrity: sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-minify-selectors@7.0.5: - resolution: {integrity: sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug==} + postcss-minify-selectors@7.0.4: + resolution: {integrity: sha512-JG55VADcNb4xFCf75hXkzc1rNeURhlo7ugf6JjiiKRfMsKlDzN9CXHZDyiG6x/zGchpjQS+UAgb1d4nqXqOpmA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 postcss-nested@6.0.1: resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} @@ -11968,77 +12015,77 @@ packages: peerDependencies: postcss: ^8.2.14 - postcss-normalize-charset@7.0.1: - resolution: {integrity: sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ==} + postcss-normalize-charset@7.0.0: + resolution: {integrity: sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-normalize-display-values@7.0.1: - resolution: {integrity: sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ==} + postcss-normalize-display-values@7.0.0: + resolution: {integrity: sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-normalize-positions@7.0.1: - resolution: {integrity: sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ==} + postcss-normalize-positions@7.0.0: + resolution: {integrity: sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-normalize-repeat-style@7.0.1: - resolution: {integrity: sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ==} + postcss-normalize-repeat-style@7.0.0: + resolution: {integrity: sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-normalize-string@7.0.1: - resolution: {integrity: sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ==} + postcss-normalize-string@7.0.0: + resolution: {integrity: sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-normalize-timing-functions@7.0.1: - resolution: {integrity: sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg==} + postcss-normalize-timing-functions@7.0.0: + resolution: {integrity: sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-normalize-unicode@7.0.4: - resolution: {integrity: sha512-LvIURTi1sQoZqj8mEIE8R15yvM+OhbR1avynMtI9bUzj5gGKR/gfZFd8O7VMj0QgJaIFzxDwxGl/ASMYAkqO8g==} + postcss-normalize-unicode@7.0.2: + resolution: {integrity: sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-normalize-url@7.0.1: - resolution: {integrity: sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ==} + postcss-normalize-url@7.0.0: + resolution: {integrity: sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-normalize-whitespace@7.0.1: - resolution: {integrity: sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA==} + postcss-normalize-whitespace@7.0.0: + resolution: {integrity: sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-ordered-values@7.0.2: - resolution: {integrity: sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw==} + postcss-ordered-values@7.0.1: + resolution: {integrity: sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-reduce-initial@7.0.4: - resolution: {integrity: sha512-rdIC9IlMBn7zJo6puim58Xd++0HdbvHeHaPgXsimMfG1ijC5A9ULvNLSE0rUKVJOvNMcwewW4Ga21ngyJjY/+Q==} + postcss-reduce-initial@7.0.2: + resolution: {integrity: sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-reduce-transforms@7.0.1: - resolution: {integrity: sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g==} + postcss-reduce-transforms@7.0.0: + resolution: {integrity: sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 postcss-selector-parser@6.1.1: resolution: {integrity: sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==} @@ -12048,21 +12095,17 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} - postcss-selector-parser@7.1.0: - resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} - engines: {node: '>=4'} - - postcss-svgo@7.1.0: - resolution: {integrity: sha512-KnAlfmhtoLz6IuU3Sij2ycusNs4jPW+QoFE5kuuUOK8awR6tMxZQrs5Ey3BUz7nFCzT3eqyFgqkyrHiaU2xx3w==} + postcss-svgo@7.0.1: + resolution: {integrity: sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==} engines: {node: ^18.12.0 || ^20.9.0 || >= 18} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 - postcss-unique-selectors@7.0.4: - resolution: {integrity: sha512-pmlZjsmEAG7cHd7uK3ZiNSW6otSZ13RHuZ/4cDN/bVglS5EpF2r2oxY99SuOHa8m7AWoBCelTS3JPpzsIs8skQ==} + postcss-unique-selectors@7.0.3: + resolution: {integrity: sha512-J+58u5Ic5T1QjP/LDV9g3Cx4CNOgB5vz+kM6+OxHHhFACdcDeKhBXjQmB7fnIZM12YSTvsL0Opwco83DmacW2g==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -12843,6 +12886,10 @@ packages: socket.io-client@2.5.0: resolution: {integrity: sha512-lOO9clmdgssDykiOmVQQitwBAF3I6mYcQAo7hQ7AM6Ny5X7fp8hIJ3HcQs3Rjz4SoggoxA1OgrQyY8EgTbcPYw==} + socket.io-client@3.1.3: + resolution: {integrity: sha512-4sIGOGOmCg3AOgGi7EEr6ZkTZRkrXwub70bBB/F0JSkMOUFpA77WsL87o34DffQQ31PkbMUIadGOk+3tx1KGbw==} + engines: {node: '>=10.0.0'} + socket.io-client@4.8.0: resolution: {integrity: sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==} engines: {node: '>=10.0.0'} @@ -12850,6 +12897,10 @@ packages: socket.io-parser@3.3.4: resolution: {integrity: sha512-z/pFQB3x+EZldRRzORYW1vwVO8m/3ILkswtnpoeU6Ve3cbMWkmHEWDAVJn4QJtchiiFTo5j7UG2QvwxvaA9vow==} + socket.io-parser@4.0.5: + resolution: {integrity: sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig==} + engines: {node: '>=10.0.0'} + socket.io-parser@4.2.4: resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} engines: {node: '>=10.0.0'} @@ -13051,11 +13102,11 @@ packages: style-mod@4.1.2: resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} - stylehacks@7.0.6: - resolution: {integrity: sha512-iitguKivmsueOmTO0wmxURXBP8uqOO+zikLGZ7Mm9e/94R4w5T999Js2taS/KBOnQ/wdC3jN3vNSrkGDrlnqQg==} + stylehacks@7.0.4: + resolution: {integrity: sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: ^8.4.32 + postcss: ^8.4.31 subscriptions-transport-ws@0.11.0: resolution: {integrity: sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ==} @@ -13106,9 +13157,9 @@ packages: resolution: {integrity: sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==} engines: {node: '>= 8'} - svgo@4.0.0: - resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==} - engines: {node: '>=16'} + svgo@3.3.2: + resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} + engines: {node: '>=14.0.0'} hasBin: true swagger-parser@10.0.3: @@ -13241,6 +13292,9 @@ packages: timers@0.1.1: resolution: {integrity: sha512-pkJC8uIP/gxDHxNQUBUbjHyl6oZfT+ofn7tbaHW+CFIUjI+Q2MBbHcx1JSBQfhDaTcO9bNg328q0i7Vk5PismQ==} + timsort@0.3.0: + resolution: {integrity: sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==} + tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} @@ -13345,6 +13399,12 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + ts-api-utils@1.3.0: + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -14583,6 +14643,18 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ws@7.4.6: + resolution: {integrity: sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@7.5.10: resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} @@ -15612,7 +15684,7 @@ snapshots: dependencies: '@babel/core': 7.25.7 '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.7) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color @@ -15621,7 +15693,7 @@ snapshots: '@babel/compat-data': 7.25.7 '@babel/core': 7.25.7 '@babel/helper-compilation-targets': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.7) '@babel/plugin-transform-parameters': 7.25.7(@babel/core@7.25.7) @@ -15681,7 +15753,7 @@ snapshots: '@babel/plugin-syntax-flow@7.25.7(@babel/core@7.25.7)': dependencies: '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-import-assertions@7.25.7(@babel/core@7.25.7)': dependencies: @@ -15726,7 +15798,7 @@ snapshots: '@babel/plugin-syntax-jsx@7.25.7(@babel/core@7.25.7)': dependencies: '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)': dependencies: @@ -16048,7 +16120,7 @@ snapshots: '@babel/plugin-transform-flow-strip-types@7.25.7(@babel/core@7.25.7)': dependencies: '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-flow': 7.25.7(@babel/core@7.25.7) '@babel/plugin-transform-for-of@7.25.7(@babel/core@7.25.7)': @@ -16358,16 +16430,16 @@ snapshots: '@babel/plugin-transform-react-display-name@7.25.7(@babel/core@7.25.7)': dependencies: '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-transform-react-jsx@7.25.7(@babel/core@7.25.7)': dependencies: '@babel/core': 7.25.7 '@babel/helper-annotate-as-pure': 7.25.7 '@babel/helper-module-imports': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.25.7) - '@babel/types': 7.25.7 + '@babel/types': 7.27.1 transitivePeerDependencies: - supports-color @@ -19810,7 +19882,7 @@ snapshots: '@tybys/wasm-util': 0.10.0 optional: true - '@nestjs-modules/mailer@2.0.2(@nestjs/common@11.1.5(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.5)(nodemailer@7.0.5)(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3)': + '@nestjs-modules/mailer@2.0.2(@nestjs/common@11.1.5(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.5)(nodemailer@7.0.5)(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3)': dependencies: '@css-inline/css-inline': 0.14.1 '@nestjs/common': 11.1.5(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -19824,7 +19896,7 @@ snapshots: ejs: 3.1.10 handlebars: 4.7.8 liquidjs: 10.17.0 - mjml: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) preview-email: 3.1.0 pug: 3.0.3 transitivePeerDependencies: @@ -20651,6 +20723,9 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@trysound/sax@0.2.0': + optional: true + '@tsconfig/node10@1.0.11': {} '@tsconfig/node12@1.0.11': {} @@ -20696,6 +20771,8 @@ snapshots: '@types/caseless@0.12.5': {} + '@types/component-emitter@1.2.14': {} + '@types/connect@3.4.38': dependencies: '@types/node': 22.9.3 @@ -20858,7 +20935,7 @@ snapshots: '@types/node-fetch@2.6.12': dependencies: '@types/node': 22.9.3 - form-data: 4.0.4 + form-data: 4.0.2 '@types/node@17.0.45': {} @@ -20992,7 +21069,7 @@ snapshots: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 '@types/node': 22.9.3 - form-data: 4.0.4 + form-data: 4.0.2 '@types/supertest@6.0.3': dependencies: @@ -21083,7 +21160,7 @@ snapshots: '@typescript-eslint/eslint-plugin@8.9.0(@typescript-eslint/parser@8.9.0(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0)(typescript@5.8.3)': dependencies: - '@eslint-community/regexpp': 4.12.1 + '@eslint-community/regexpp': 4.11.1 '@typescript-eslint/parser': 8.9.0(eslint@8.57.0)(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.9.0 '@typescript-eslint/type-utils': 8.9.0(eslint@8.57.0)(typescript@5.8.3) @@ -21093,7 +21170,7 @@ snapshots: graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.4.3(typescript@5.8.3) + ts-api-utils: 1.3.0(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -21142,7 +21219,7 @@ snapshots: '@typescript-eslint/types': 8.9.0 '@typescript-eslint/typescript-estree': 8.9.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.9.0 - debug: 4.4.1 + debug: 4.3.7 eslint: 8.57.0 optionalDependencies: typescript: 5.8.3 @@ -21228,7 +21305,7 @@ snapshots: '@typescript-eslint/typescript-estree': 8.9.0(typescript@5.8.3) '@typescript-eslint/utils': 8.9.0(eslint@8.57.0)(typescript@5.8.3) debug: 4.4.1 - ts-api-utils: 1.4.3(typescript@5.8.3) + ts-api-utils: 1.3.0(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -21312,7 +21389,7 @@ snapshots: fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.3 + semver: 7.7.2 ts-api-utils: 1.4.3(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 @@ -22125,6 +22202,10 @@ snapshots: mime-types: 3.0.1 negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.12.1): + dependencies: + acorn: 8.12.1 + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: acorn: 8.14.1 @@ -22328,7 +22409,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.23.3 es-errors: 1.3.0 - get-intrinsic: 1.3.0 + get-intrinsic: 1.2.4 is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.3 @@ -22407,6 +22488,16 @@ snapshots: postcss: 8.4.49 postcss-value-parser: 4.2.0 + autoprefixer@10.4.21(postcss@8.4.47): + dependencies: + browserslist: 4.24.5 + caniuse-lite: 1.0.30001718 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.4.47 + postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.0.0 @@ -22416,7 +22507,7 @@ snapshots: axios@1.8.2: dependencies: follow-redirects: 1.15.6 - form-data: 4.0.4 + form-data: 4.0.1 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -22664,13 +22755,6 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.5) - browserslist@4.25.1: - dependencies: - caniuse-lite: 1.0.30001727 - electron-to-chromium: 1.5.191 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.1) - bs-logger@0.2.6: dependencies: fast-json-stable-stringify: 2.1.0 @@ -22723,10 +22807,10 @@ snapshots: call-bind@1.0.7: dependencies: - es-define-property: 1.0.1 + es-define-property: 1.0.0 es-errors: 1.3.0 function-bind: 1.1.2 - get-intrinsic: 1.3.0 + get-intrinsic: 1.2.4 set-function-length: 1.2.2 call-bind@1.0.8: @@ -22758,7 +22842,7 @@ snapshots: caniuse-api@3.0.0: dependencies: - browserslist: 4.25.1 + browserslist: 4.24.5 caniuse-lite: 1.0.30001718 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 @@ -22768,8 +22852,6 @@ snapshots: caniuse-lite@1.0.30001718: {} - caniuse-lite@1.0.30001727: {} - canvas@2.11.2: dependencies: '@mapbox/node-pre-gyp': 1.0.11 @@ -23033,9 +23115,6 @@ snapshots: commander@10.0.1: optional: true - commander@11.1.0: - optional: true - commander@12.1.0: {} commander@2.20.3: {} @@ -23163,7 +23242,7 @@ snapshots: core-js-compat@3.42.0: dependencies: - browserslist: 4.25.1 + browserslist: 4.24.5 core-js@3.38.1: {} @@ -23265,9 +23344,9 @@ snapshots: source-map-js: 1.2.1 optional: true - css-tree@3.1.0: + css-tree@2.3.1: dependencies: - mdn-data: 2.12.2 + mdn-data: 2.0.30 source-map-js: 1.2.1 optional: true @@ -23278,50 +23357,50 @@ snapshots: cssfilter@0.0.10: {} - cssnano-preset-default@7.0.8(postcss@8.4.49): + cssnano-preset-default@7.0.6(postcss@8.4.49): dependencies: - browserslist: 4.25.1 + browserslist: 4.24.5 css-declaration-sorter: 7.2.0(postcss@8.4.49) - cssnano-utils: 5.0.1(postcss@8.4.49) + cssnano-utils: 5.0.0(postcss@8.4.49) postcss: 8.4.49 - postcss-calc: 10.1.1(postcss@8.4.49) - postcss-colormin: 7.0.4(postcss@8.4.49) - postcss-convert-values: 7.0.6(postcss@8.4.49) - postcss-discard-comments: 7.0.4(postcss@8.4.49) - postcss-discard-duplicates: 7.0.2(postcss@8.4.49) - postcss-discard-empty: 7.0.1(postcss@8.4.49) - postcss-discard-overridden: 7.0.1(postcss@8.4.49) - postcss-merge-longhand: 7.0.5(postcss@8.4.49) - postcss-merge-rules: 7.0.6(postcss@8.4.49) - postcss-minify-font-values: 7.0.1(postcss@8.4.49) - postcss-minify-gradients: 7.0.1(postcss@8.4.49) - postcss-minify-params: 7.0.4(postcss@8.4.49) - postcss-minify-selectors: 7.0.5(postcss@8.4.49) - postcss-normalize-charset: 7.0.1(postcss@8.4.49) - postcss-normalize-display-values: 7.0.1(postcss@8.4.49) - postcss-normalize-positions: 7.0.1(postcss@8.4.49) - postcss-normalize-repeat-style: 7.0.1(postcss@8.4.49) - postcss-normalize-string: 7.0.1(postcss@8.4.49) - postcss-normalize-timing-functions: 7.0.1(postcss@8.4.49) - postcss-normalize-unicode: 7.0.4(postcss@8.4.49) - postcss-normalize-url: 7.0.1(postcss@8.4.49) - postcss-normalize-whitespace: 7.0.1(postcss@8.4.49) - postcss-ordered-values: 7.0.2(postcss@8.4.49) - postcss-reduce-initial: 7.0.4(postcss@8.4.49) - postcss-reduce-transforms: 7.0.1(postcss@8.4.49) - postcss-svgo: 7.1.0(postcss@8.4.49) - postcss-unique-selectors: 7.0.4(postcss@8.4.49) + postcss-calc: 10.0.2(postcss@8.4.49) + postcss-colormin: 7.0.2(postcss@8.4.49) + postcss-convert-values: 7.0.4(postcss@8.4.49) + postcss-discard-comments: 7.0.3(postcss@8.4.49) + postcss-discard-duplicates: 7.0.1(postcss@8.4.49) + postcss-discard-empty: 7.0.0(postcss@8.4.49) + postcss-discard-overridden: 7.0.0(postcss@8.4.49) + postcss-merge-longhand: 7.0.4(postcss@8.4.49) + postcss-merge-rules: 7.0.4(postcss@8.4.49) + postcss-minify-font-values: 7.0.0(postcss@8.4.49) + postcss-minify-gradients: 7.0.0(postcss@8.4.49) + postcss-minify-params: 7.0.2(postcss@8.4.49) + postcss-minify-selectors: 7.0.4(postcss@8.4.49) + postcss-normalize-charset: 7.0.0(postcss@8.4.49) + postcss-normalize-display-values: 7.0.0(postcss@8.4.49) + postcss-normalize-positions: 7.0.0(postcss@8.4.49) + postcss-normalize-repeat-style: 7.0.0(postcss@8.4.49) + postcss-normalize-string: 7.0.0(postcss@8.4.49) + postcss-normalize-timing-functions: 7.0.0(postcss@8.4.49) + postcss-normalize-unicode: 7.0.2(postcss@8.4.49) + postcss-normalize-url: 7.0.0(postcss@8.4.49) + postcss-normalize-whitespace: 7.0.0(postcss@8.4.49) + postcss-ordered-values: 7.0.1(postcss@8.4.49) + postcss-reduce-initial: 7.0.2(postcss@8.4.49) + postcss-reduce-transforms: 7.0.0(postcss@8.4.49) + postcss-svgo: 7.0.1(postcss@8.4.49) + postcss-unique-selectors: 7.0.3(postcss@8.4.49) optional: true - cssnano-utils@5.0.1(postcss@8.4.49): + cssnano-utils@5.0.0(postcss@8.4.49): dependencies: postcss: 8.4.49 optional: true - cssnano@7.1.0(postcss@8.4.49): + cssnano@7.0.6(postcss@8.4.49): dependencies: - cssnano-preset-default: 7.0.8(postcss@8.4.49) - lilconfig: 3.1.3 + cssnano-preset-default: 7.0.6(postcss@8.4.49) + lilconfig: 3.1.2 postcss: 8.4.49 optional: true @@ -23488,9 +23567,9 @@ snapshots: define-data-property@1.1.4: dependencies: - es-define-property: 1.0.1 + es-define-property: 1.0.0 es-errors: 1.3.0 - gopd: 1.2.0 + gopd: 1.0.1 define-lazy-prop@3.0.0: {} @@ -23630,6 +23709,8 @@ snapshots: dotenv@16.4.7: {} + dotenv@17.2.1: {} + dset@3.1.4: {} dunder-proto@1.0.1: @@ -23658,8 +23739,6 @@ snapshots: electron-to-chromium@1.5.155: {} - electron-to-chromium@1.5.191: {} - electron-to-chromium@1.5.35: {} emittery@0.13.1: {} @@ -23702,6 +23781,23 @@ snapshots: - supports-color - utf-8-validate + engine.io-client@4.1.4: + dependencies: + base64-arraybuffer: 0.1.4 + component-emitter: 1.3.1 + debug: 4.3.7 + engine.io-parser: 4.0.3 + has-cors: 1.1.0 + parseqs: 0.0.6 + parseuri: 0.0.6 + ws: 7.4.6 + xmlhttprequest-ssl: 1.6.3 + yeast: 0.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + engine.io-client@6.6.1: dependencies: '@socket.io/component-emitter': 3.1.2 @@ -23722,6 +23818,10 @@ snapshots: blob: 0.0.5 has-binary2: 1.0.3 + engine.io-parser@4.0.3: + dependencies: + base64-arraybuffer: 0.1.4 + engine.io-parser@5.2.3: {} enhanced-resolve@2.3.0: @@ -23767,19 +23867,19 @@ snapshots: 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.1 + es-define-property: 1.0.0 es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.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.3.0 + get-intrinsic: 1.2.4 get-symbol-description: 1.0.2 globalthis: 1.0.4 - gopd: 1.2.0 + gopd: 1.0.1 has-property-descriptors: 1.0.2 has-proto: 1.0.3 - has-symbols: 1.1.0 + has-symbols: 1.0.3 hasown: 2.0.2 internal-slot: 1.0.7 is-array-buffer: 3.0.4 @@ -23861,6 +23961,10 @@ snapshots: unbox-primitive: 1.1.0 which-typed-array: 1.1.19 + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -23868,8 +23972,8 @@ snapshots: es-get-iterator@1.1.3: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 is-arguments: 1.1.1 is-map: 2.0.3 is-set: 2.0.3 @@ -23887,6 +23991,12 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.0.3: + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + es-set-tostringtag@2.1.0: dependencies: es-errors: 1.3.0 @@ -24223,7 +24333,7 @@ snapshots: eslint-plugin-vue@9.29.0(eslint@8.57.0): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) eslint: 8.57.0 globals: 13.24.0 natural-compare: 1.4.0 @@ -24397,8 +24507,8 @@ snapshots: espree@9.6.1: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) eslint-visitor-keys: 3.4.3 esprima-extract-comments@1.1.0: @@ -24852,6 +24962,19 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + form-data@4.0.4: dependencies: asynckit: 0.4.0 @@ -24959,7 +25082,7 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 has-proto: 1.0.3 - has-symbols: 1.1.0 + has-symbols: 1.0.3 hasown: 2.0.2 get-intrinsic@1.3.0: @@ -25000,7 +25123,7 @@ snapshots: dependencies: call-bind: 1.0.7 es-errors: 1.3.0 - get-intrinsic: 1.3.0 + get-intrinsic: 1.2.4 get-symbol-description@1.1.0: dependencies: @@ -25347,7 +25470,7 @@ snapshots: has-property-descriptors@1.0.2: dependencies: - es-define-property: 1.0.1 + es-define-property: 1.0.0 has-proto@1.0.3: {} @@ -25355,11 +25478,13 @@ snapshots: dependencies: dunder-proto: 1.0.1 + has-symbols@1.0.3: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: dependencies: - has-symbols: 1.1.0 + has-symbols: 1.0.3 has-unicode@2.0.1: optional: true @@ -25405,14 +25530,16 @@ snapshots: selderee: 0.11.0 optional: true - htmlnano@2.1.2(cssnano@7.1.0(postcss@8.4.49))(postcss@8.4.49)(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + htmlnano@2.1.1(cssnano@7.0.6(postcss@8.4.49))(postcss@8.4.49)(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: cosmiconfig: 9.0.0(typescript@5.8.3) posthtml: 0.16.6 + timsort: 0.3.0 optionalDependencies: - cssnano: 7.1.0(postcss@8.4.49) + cssnano: 7.0.6(postcss@8.4.49) postcss: 8.4.49 relateurl: 0.2.7 + svgo: 3.3.2 terser: 5.39.2 transitivePeerDependencies: - typescript @@ -25664,7 +25791,7 @@ snapshots: is-array-buffer@3.0.4: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.3.0 + get-intrinsic: 1.2.4 is-array-buffer@3.0.5: dependencies: @@ -25862,7 +25989,7 @@ snapshots: is-symbol@1.0.4: dependencies: - has-symbols: 1.1.0 + has-symbols: 1.0.3 is-symbol@1.1.1: dependencies: @@ -25907,7 +26034,7 @@ snapshots: is-weakset@2.0.3: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.3.0 + get-intrinsic: 1.2.4 is-what@4.1.16: {} @@ -26435,7 +26562,7 @@ snapshots: cssstyle: 4.1.0 data-urls: 5.0.0 decimal.js: 10.4.3 - form-data: 4.0.4 + form-data: 4.0.1 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.5 @@ -26616,9 +26743,6 @@ snapshots: lilconfig@3.1.2: {} - lilconfig@3.1.3: - optional: true - lines-and-columns@1.2.4: {} linkify-it@5.0.0: @@ -26881,7 +27005,7 @@ snapshots: mdn-data@2.0.28: optional: true - mdn-data@2.12.2: + mdn-data@2.0.30: optional: true mdurl@2.0.0: {} @@ -27006,11 +27130,11 @@ snapshots: yallist: 4.0.0 optional: true - mjml-accordion@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-accordion@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27022,11 +27146,11 @@ snapshots: - uncss optional: true - mjml-body@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-body@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27038,11 +27162,11 @@ snapshots: - uncss optional: true - mjml-button@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-button@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27054,11 +27178,11 @@ snapshots: - uncss optional: true - mjml-carousel@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-carousel@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27070,14 +27194,14 @@ snapshots: - uncss optional: true - mjml-cli@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-cli@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 chokidar: 3.6.0 glob: 10.4.5 lodash: 4.17.21 minimatch: 9.0.5 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) mjml-parser-xml: 5.0.0-alpha.4 mjml-validator: 5.0.0-alpha.4 yargs: 17.7.2 @@ -27092,11 +27216,11 @@ snapshots: - uncss optional: true - mjml-column@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-column@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27108,13 +27232,13 @@ snapshots: - uncss optional: true - mjml-core@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-core@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 cheerio: 1.0.0-rc.12 - cssnano: 7.1.0(postcss@8.4.49) + cssnano: 7.0.6(postcss@8.4.49) detect-node: 2.1.0 - htmlnano: 2.1.2(cssnano@7.1.0(postcss@8.4.49))(postcss@8.4.49)(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + htmlnano: 2.1.1(cssnano@7.0.6(postcss@8.4.49))(postcss@8.4.49)(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) juice: 10.0.1 lodash: 4.17.21 mjml-parser-xml: 5.0.0-alpha.4 @@ -27132,11 +27256,11 @@ snapshots: - uncss optional: true - mjml-divider@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-divider@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27148,11 +27272,11 @@ snapshots: - uncss optional: true - mjml-group@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-group@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27164,11 +27288,11 @@ snapshots: - uncss optional: true - mjml-head-attributes@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-head-attributes@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27180,11 +27304,11 @@ snapshots: - uncss optional: true - mjml-head-breakpoint@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-head-breakpoint@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27196,11 +27320,11 @@ snapshots: - uncss optional: true - mjml-head-font@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-head-font@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27212,11 +27336,11 @@ snapshots: - uncss optional: true - mjml-head-html-attributes@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-head-html-attributes@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27228,11 +27352,11 @@ snapshots: - uncss optional: true - mjml-head-preview@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-head-preview@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27244,11 +27368,11 @@ snapshots: - uncss optional: true - mjml-head-style@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-head-style@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27260,11 +27384,11 @@ snapshots: - uncss optional: true - mjml-head-title@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-head-title@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27276,11 +27400,11 @@ snapshots: - uncss optional: true - mjml-head@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-head@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27292,11 +27416,11 @@ snapshots: - uncss optional: true - mjml-hero@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-hero@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27308,11 +27432,11 @@ snapshots: - uncss optional: true - mjml-image@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-image@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27324,11 +27448,11 @@ snapshots: - uncss optional: true - mjml-navbar@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-navbar@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27348,34 +27472,34 @@ snapshots: lodash: 4.17.21 optional: true - mjml-preset-core@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-preset-core@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 - mjml-accordion: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-body: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-button: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-carousel: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-column: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-divider: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-group: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-head: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-head-attributes: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-head-breakpoint: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-head-font: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-head-html-attributes: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-head-preview: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-head-style: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-head-title: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-hero: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-image: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-navbar: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-raw: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-section: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-social: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-spacer: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-table: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-text: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-wrapper: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-accordion: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-body: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-button: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-carousel: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-column: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-divider: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-group: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-head: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-head-attributes: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-head-breakpoint: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-head-font: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-head-html-attributes: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-head-preview: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-head-style: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-head-title: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-hero: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-image: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-navbar: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-raw: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-section: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-social: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-spacer: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-table: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-text: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-wrapper: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27387,11 +27511,11 @@ snapshots: - uncss optional: true - mjml-raw@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-raw@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27403,11 +27527,11 @@ snapshots: - uncss optional: true - mjml-section@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-section@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27419,11 +27543,11 @@ snapshots: - uncss optional: true - mjml-social@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-social@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27435,11 +27559,11 @@ snapshots: - uncss optional: true - mjml-spacer@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-spacer@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27451,11 +27575,11 @@ snapshots: - uncss optional: true - mjml-table@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-table@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27467,11 +27591,11 @@ snapshots: - uncss optional: true - mjml-text@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-text@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27488,12 +27612,12 @@ snapshots: '@babel/runtime': 7.26.10 optional: true - mjml-wrapper@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml-wrapper@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 lodash: 4.17.21 - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-section: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-section: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) transitivePeerDependencies: - encoding - purgecss @@ -27505,12 +27629,12 @@ snapshots: - uncss optional: true - mjml@5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3): + mjml@5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 - mjml-cli: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) - mjml-preset-core: 5.0.0-alpha.4(relateurl@0.2.7)(terser@5.39.2)(typescript@5.8.3) + mjml-cli: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) + mjml-preset-core: 5.0.0-alpha.4(relateurl@0.2.7)(svgo@3.3.2)(terser@5.39.2)(typescript@5.8.3) mjml-validator: 5.0.0-alpha.4 transitivePeerDependencies: - encoding @@ -27581,6 +27705,16 @@ snapshots: monaco-editor@0.52.2: {} + morgan@1.10.1: + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.1.0 + transitivePeerDependencies: + - supports-color + mrmime@2.0.0: {} ms@2.0.0: {} @@ -27800,7 +27934,7 @@ snapshots: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - has-symbols: 1.1.0 + has-symbols: 1.0.3 object-keys: 1.1.1 object.assign@4.1.7: @@ -27812,6 +27946,10 @@ snapshots: has-symbols: 1.1.0 object-keys: 1.1.1 + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -28188,46 +28326,46 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-calc@10.1.1(postcss@8.4.49): + postcss-calc@10.0.2(postcss@8.4.49): dependencies: postcss: 8.4.49 - postcss-selector-parser: 7.1.0 + postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 optional: true - postcss-colormin@7.0.4(postcss@8.4.49): + postcss-colormin@7.0.2(postcss@8.4.49): dependencies: - browserslist: 4.25.1 + browserslist: 4.24.5 caniuse-api: 3.0.0 colord: 2.9.3 postcss: 8.4.49 postcss-value-parser: 4.2.0 optional: true - postcss-convert-values@7.0.6(postcss@8.4.49): + postcss-convert-values@7.0.4(postcss@8.4.49): dependencies: - browserslist: 4.25.1 + browserslist: 4.24.5 postcss: 8.4.49 postcss-value-parser: 4.2.0 optional: true - postcss-discard-comments@7.0.4(postcss@8.4.49): + postcss-discard-comments@7.0.3(postcss@8.4.49): dependencies: postcss: 8.4.49 - postcss-selector-parser: 7.1.0 + postcss-selector-parser: 6.1.2 optional: true - postcss-discard-duplicates@7.0.2(postcss@8.4.49): + postcss-discard-duplicates@7.0.1(postcss@8.4.49): dependencies: postcss: 8.4.49 optional: true - postcss-discard-empty@7.0.1(postcss@8.4.49): + postcss-discard-empty@7.0.0(postcss@8.4.49): dependencies: postcss: 8.4.49 optional: true - postcss-discard-overridden@7.0.1(postcss@8.4.49): + postcss-discard-overridden@7.0.0(postcss@8.4.49): dependencies: postcss: 8.4.49 optional: true @@ -28296,49 +28434,49 @@ snapshots: postcss: 8.4.49 yaml: 2.5.1 - postcss-merge-longhand@7.0.5(postcss@8.4.49): + postcss-merge-longhand@7.0.4(postcss@8.4.49): dependencies: postcss: 8.4.49 postcss-value-parser: 4.2.0 - stylehacks: 7.0.6(postcss@8.4.49) + stylehacks: 7.0.4(postcss@8.4.49) optional: true - postcss-merge-rules@7.0.6(postcss@8.4.49): + postcss-merge-rules@7.0.4(postcss@8.4.49): dependencies: - browserslist: 4.25.1 + browserslist: 4.24.5 caniuse-api: 3.0.0 - cssnano-utils: 5.0.1(postcss@8.4.49) + cssnano-utils: 5.0.0(postcss@8.4.49) postcss: 8.4.49 - postcss-selector-parser: 7.1.0 + postcss-selector-parser: 6.1.2 optional: true - postcss-minify-font-values@7.0.1(postcss@8.4.49): + postcss-minify-font-values@7.0.0(postcss@8.4.49): dependencies: postcss: 8.4.49 postcss-value-parser: 4.2.0 optional: true - postcss-minify-gradients@7.0.1(postcss@8.4.49): + postcss-minify-gradients@7.0.0(postcss@8.4.49): dependencies: colord: 2.9.3 - cssnano-utils: 5.0.1(postcss@8.4.49) + cssnano-utils: 5.0.0(postcss@8.4.49) postcss: 8.4.49 postcss-value-parser: 4.2.0 optional: true - postcss-minify-params@7.0.4(postcss@8.4.49): + postcss-minify-params@7.0.2(postcss@8.4.49): dependencies: - browserslist: 4.25.1 - cssnano-utils: 5.0.1(postcss@8.4.49) + browserslist: 4.24.5 + cssnano-utils: 5.0.0(postcss@8.4.49) postcss: 8.4.49 postcss-value-parser: 4.2.0 optional: true - postcss-minify-selectors@7.0.5(postcss@8.4.49): + postcss-minify-selectors@7.0.4(postcss@8.4.49): dependencies: cssesc: 3.0.0 postcss: 8.4.49 - postcss-selector-parser: 7.1.0 + postcss-selector-parser: 6.1.2 optional: true postcss-nested@6.0.1(postcss@8.4.47): @@ -28351,75 +28489,75 @@ snapshots: postcss: 8.4.49 postcss-selector-parser: 6.1.1 - postcss-normalize-charset@7.0.1(postcss@8.4.49): + postcss-normalize-charset@7.0.0(postcss@8.4.49): dependencies: postcss: 8.4.49 optional: true - postcss-normalize-display-values@7.0.1(postcss@8.4.49): + postcss-normalize-display-values@7.0.0(postcss@8.4.49): dependencies: postcss: 8.4.49 postcss-value-parser: 4.2.0 optional: true - postcss-normalize-positions@7.0.1(postcss@8.4.49): + postcss-normalize-positions@7.0.0(postcss@8.4.49): dependencies: postcss: 8.4.49 postcss-value-parser: 4.2.0 optional: true - postcss-normalize-repeat-style@7.0.1(postcss@8.4.49): + postcss-normalize-repeat-style@7.0.0(postcss@8.4.49): dependencies: postcss: 8.4.49 postcss-value-parser: 4.2.0 optional: true - postcss-normalize-string@7.0.1(postcss@8.4.49): + postcss-normalize-string@7.0.0(postcss@8.4.49): dependencies: postcss: 8.4.49 postcss-value-parser: 4.2.0 optional: true - postcss-normalize-timing-functions@7.0.1(postcss@8.4.49): + postcss-normalize-timing-functions@7.0.0(postcss@8.4.49): dependencies: postcss: 8.4.49 postcss-value-parser: 4.2.0 optional: true - postcss-normalize-unicode@7.0.4(postcss@8.4.49): + postcss-normalize-unicode@7.0.2(postcss@8.4.49): dependencies: - browserslist: 4.25.1 + browserslist: 4.24.5 postcss: 8.4.49 postcss-value-parser: 4.2.0 optional: true - postcss-normalize-url@7.0.1(postcss@8.4.49): + postcss-normalize-url@7.0.0(postcss@8.4.49): dependencies: postcss: 8.4.49 postcss-value-parser: 4.2.0 optional: true - postcss-normalize-whitespace@7.0.1(postcss@8.4.49): + postcss-normalize-whitespace@7.0.0(postcss@8.4.49): dependencies: postcss: 8.4.49 postcss-value-parser: 4.2.0 optional: true - postcss-ordered-values@7.0.2(postcss@8.4.49): + postcss-ordered-values@7.0.1(postcss@8.4.49): dependencies: - cssnano-utils: 5.0.1(postcss@8.4.49) + cssnano-utils: 5.0.0(postcss@8.4.49) postcss: 8.4.49 postcss-value-parser: 4.2.0 optional: true - postcss-reduce-initial@7.0.4(postcss@8.4.49): + postcss-reduce-initial@7.0.2(postcss@8.4.49): dependencies: - browserslist: 4.25.1 + browserslist: 4.24.5 caniuse-api: 3.0.0 postcss: 8.4.49 optional: true - postcss-reduce-transforms@7.0.1(postcss@8.4.49): + postcss-reduce-transforms@7.0.0(postcss@8.4.49): dependencies: postcss: 8.4.49 postcss-value-parser: 4.2.0 @@ -28435,23 +28573,17 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@7.1.0: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - optional: true - - postcss-svgo@7.1.0(postcss@8.4.49): + postcss-svgo@7.0.1(postcss@8.4.49): dependencies: postcss: 8.4.49 postcss-value-parser: 4.2.0 - svgo: 4.0.0 + svgo: 3.3.2 optional: true - postcss-unique-selectors@7.0.4(postcss@8.4.49): + postcss-unique-selectors@7.0.3(postcss@8.4.49): dependencies: postcss: 8.4.49 - postcss-selector-parser: 7.1.0 + postcss-selector-parser: 6.1.2 optional: true postcss-value-parser@4.2.0: {} @@ -29070,8 +29202,8 @@ snapshots: safe-array-concat@1.1.2: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 isarray: 2.0.5 safe-array-concat@1.1.3: @@ -29216,8 +29348,8 @@ snapshots: define-data-property: 1.1.4 es-errors: 1.3.0 function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 + get-intrinsic: 1.2.4 + gopd: 1.0.1 has-property-descriptors: 1.0.2 set-function-name@2.0.2: @@ -29399,6 +29531,20 @@ snapshots: - supports-color - utf-8-validate + socket.io-client@3.1.3: + dependencies: + '@types/component-emitter': 1.2.14 + backo2: 1.0.2 + component-emitter: 1.3.1 + debug: 4.3.7 + engine.io-client: 4.1.4 + parseuri: 0.0.6 + socket.io-parser: 4.0.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + socket.io-client@4.8.0: dependencies: '@socket.io/component-emitter': 3.1.2 @@ -29418,6 +29564,14 @@ snapshots: transitivePeerDependencies: - supports-color + socket.io-parser@4.0.5: + dependencies: + '@types/component-emitter': 1.2.14 + component-emitter: 1.3.1 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + socket.io-parser@4.2.4: dependencies: '@socket.io/component-emitter': 3.1.2 @@ -29575,13 +29729,13 @@ snapshots: call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.23.3 - es-object-atoms: 1.1.1 + es-object-atoms: 1.0.0 string.prototype.trimend@1.0.8: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.0.0 string.prototype.trimend@1.0.9: dependencies: @@ -29594,7 +29748,7 @@ snapshots: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.0.0 string_decoder@1.1.1: dependencies: @@ -29639,11 +29793,11 @@ snapshots: style-mod@4.1.2: {} - stylehacks@7.0.6(postcss@8.4.49): + stylehacks@7.0.4(postcss@8.4.49): dependencies: - browserslist: 4.25.1 + browserslist: 4.24.5 postcss: 8.4.49 - postcss-selector-parser: 7.1.0 + postcss-selector-parser: 6.1.2 optional: true subscriptions-transport-ws@0.11.0(graphql@16.11.0): @@ -29725,15 +29879,15 @@ snapshots: svelte@3.59.2: {} - svgo@4.0.0: + svgo@3.3.2: dependencies: - commander: 11.1.0 + '@trysound/sax': 0.2.0 + commander: 7.2.0 css-select: 5.1.0 - css-tree: 3.1.0 + css-tree: 2.3.1 css-what: 6.1.0 csso: 5.0.5 picocolors: 1.1.1 - sax: 1.4.1 optional: true swagger-parser@10.0.3(openapi-types@12.1.3): @@ -29996,6 +30150,9 @@ snapshots: timers@0.1.1: {} + timsort@0.3.0: + optional: true + tiny-inflate@1.0.3: {} tiny-invariant@1.3.3: {} @@ -30081,6 +30238,10 @@ snapshots: tree-kill@1.2.2: {} + ts-api-utils@1.3.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + ts-api-utils@1.4.3(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -30323,7 +30484,7 @@ snapshots: dependencies: call-bind: 1.0.7 for-each: 0.3.3 - gopd: 1.2.0 + gopd: 1.0.1 has-proto: 1.0.3 is-typed-array: 1.1.13 @@ -30340,7 +30501,7 @@ snapshots: available-typed-arrays: 1.0.7 call-bind: 1.0.7 for-each: 0.3.3 - gopd: 1.2.0 + gopd: 1.0.1 has-proto: 1.0.3 is-typed-array: 1.1.13 @@ -30358,7 +30519,7 @@ snapshots: dependencies: call-bind: 1.0.7 for-each: 0.3.3 - gopd: 1.2.0 + gopd: 1.0.1 has-proto: 1.0.3 is-typed-array: 1.1.13 possible-typed-array-names: 1.0.0 @@ -30401,7 +30562,7 @@ snapshots: dependencies: call-bind: 1.0.7 has-bigints: 1.0.2 - has-symbols: 1.1.0 + has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 unbox-primitive@1.1.0: @@ -30655,12 +30816,6 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - update-browserslist-db@1.1.3(browserslist@4.25.1): - dependencies: - browserslist: 4.25.1 - escalade: 3.2.0 - picocolors: 1.1.1 - upper-case-first@2.0.2: dependencies: tslib: 2.8.1 @@ -31806,6 +31961,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + ws@7.4.6: {} + ws@7.5.10: {} ws@8.13.0: {}