From e4332110d455a3012d5c77a9186bc4aa096e34f2 Mon Sep 17 00:00:00 2001 From: Mir Arif Hasan Date: Wed, 10 Jun 2026 11:04:18 +0600 Subject: [PATCH 01/23] fix(mock-server): persist isPublic on creation, default to private (#6410) * fix(mock-server): persist isPublic on creation, default to private * test: update unit test coverage --- .../src/mock-server/mock-server.model.ts | 4 +- .../mock-server/mock-server.service.spec.ts | 67 +++++++++++++++++++ .../src/mock-server/mock-server.service.ts | 1 + 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/hoppscotch-backend/src/mock-server/mock-server.model.ts b/packages/hoppscotch-backend/src/mock-server/mock-server.model.ts index e53a76aa4dc..787c33645a4 100644 --- a/packages/hoppscotch-backend/src/mock-server/mock-server.model.ts +++ b/packages/hoppscotch-backend/src/mock-server/mock-server.model.ts @@ -179,8 +179,8 @@ export class CreateMockServerInput { @IsOptional() @Field({ nullable: true, - defaultValue: true, - description: 'Whether the mock server is publicly accessible', + description: + 'Whether the mock server is publicly accessible (defaults to false/private if omitted)', }) isPublic?: boolean; } diff --git a/packages/hoppscotch-backend/src/mock-server/mock-server.service.spec.ts b/packages/hoppscotch-backend/src/mock-server/mock-server.service.spec.ts index 646f15f57eb..7bab3c0525b 100644 --- a/packages/hoppscotch-backend/src/mock-server/mock-server.service.spec.ts +++ b/packages/hoppscotch-backend/src/mock-server/mock-server.service.spec.ts @@ -389,6 +389,73 @@ describe('MockServerService', () => { ); }); + // Regression: GHSA-c68f-wr5p-j6jf — isPublic from input must be persisted, + // and must default to private (false) when omitted. + test('should persist isPublic: false when input requests a private server', async () => { + mockPrisma.userCollection.findUnique.mockResolvedValue(userCollection); + mockPrisma.mockServer.findUnique.mockResolvedValue(null); + mockPrisma.mockServer.create.mockResolvedValue({ + ...dbMockServer, + isPublic: false, + }); + + const result = await mockServerService.createMockServer(user, { + ...createInput, + isPublic: false, + }); + + expect(mockPrisma.mockServer.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ isPublic: false }), + }), + ); + expect(E.isRight(result)).toBe(true); + if (E.isRight(result)) { + expect((result.right as any).isPublic).toBe(false); + } + }); + + test('should persist isPublic: true when input requests a public server', async () => { + mockPrisma.userCollection.findUnique.mockResolvedValue(userCollection); + mockPrisma.mockServer.findUnique.mockResolvedValue(null); + mockPrisma.mockServer.create.mockResolvedValue({ + ...dbMockServer, + isPublic: true, + }); + + const result = await mockServerService.createMockServer(user, { + ...createInput, + isPublic: true, + }); + + expect(mockPrisma.mockServer.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ isPublic: true }), + }), + ); + expect(E.isRight(result)).toBe(true); + if (E.isRight(result)) { + expect((result.right as any).isPublic).toBe(true); + } + }); + + test('should default to private (isPublic: false) when input omits isPublic', async () => { + mockPrisma.userCollection.findUnique.mockResolvedValue(userCollection); + mockPrisma.mockServer.findUnique.mockResolvedValue(null); + mockPrisma.mockServer.create.mockResolvedValue({ + ...dbMockServer, + isPublic: false, + }); + + await mockServerService.createMockServer(user, createInput); + + expect(mockPrisma.mockServer.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ isPublic: false }), + }), + ); + }); + test('should create team mock server successfully', async () => { const teamInput: CreateMockServerInput = { name: 'Team Mock Server', diff --git a/packages/hoppscotch-backend/src/mock-server/mock-server.service.ts b/packages/hoppscotch-backend/src/mock-server/mock-server.service.ts index 07980e85e18..9a829a713ea 100644 --- a/packages/hoppscotch-backend/src/mock-server/mock-server.service.ts +++ b/packages/hoppscotch-backend/src/mock-server/mock-server.service.ts @@ -421,6 +421,7 @@ export class MockServerService { ? input.workspaceID : user.uid, delayInMs: input.delayInMs, + isPublic: input.isPublic ?? false, }, }); this.mockServerAnalyticsService.recordActivity( From 9cc980bc4feb1f8e139b23b9de0beed0db72d4b7 Mon Sep 17 00:00:00 2001 From: Mir Arif Hasan Date: Wed, 10 Jun 2026 18:45:10 +0600 Subject: [PATCH 02/23] fix(backend): enforce ownership on user history and private User fields (#6409) * fix(backend): enforce ownership on user history and private User fields * test: fix unit test cases * fix(backend): gate private User field resolvers to their owner * fix(backend): restore explicit auth guard on owner-gated User fields --- .../src/mock-server/mock-server.resolver.ts | 4 +- .../published-docs.service.spec.ts | 4 +- .../published-docs/published-docs.service.ts | 4 +- .../team-invitation.resolver.ts | 4 +- .../src/team/team-member.resolver.ts | 4 +- .../src/user-environment/user.resolver.ts | 16 +++- .../user-history/user-history.service.spec.ts | 79 +++++++++++++++++++ .../src/user-history/user-history.service.ts | 12 ++- .../src/user-history/user.resolver.ts | 9 +++ .../src/user-settings/user.resolver.ts | 8 +- 10 files changed, 128 insertions(+), 16 deletions(-) diff --git a/packages/hoppscotch-backend/src/mock-server/mock-server.resolver.ts b/packages/hoppscotch-backend/src/mock-server/mock-server.resolver.ts index 423e814e6c6..cba6f308170 100644 --- a/packages/hoppscotch-backend/src/mock-server/mock-server.resolver.ts +++ b/packages/hoppscotch-backend/src/mock-server/mock-server.resolver.ts @@ -48,8 +48,8 @@ export class MockServerResolver { if (E.isLeft(creator)) throwErr(creator.left); return { ...creator.right, - currentGQLSession: JSON.stringify(creator.right.currentGQLSession), - currentRESTSession: JSON.stringify(creator.right.currentRESTSession), + currentGQLSession: null, + currentRESTSession: null, }; } diff --git a/packages/hoppscotch-backend/src/published-docs/published-docs.service.spec.ts b/packages/hoppscotch-backend/src/published-docs/published-docs.service.spec.ts index 1b1680235fb..3b43b6fe125 100644 --- a/packages/hoppscotch-backend/src/published-docs/published-docs.service.spec.ts +++ b/packages/hoppscotch-backend/src/published-docs/published-docs.service.spec.ts @@ -856,8 +856,8 @@ describe('getPublishedDocsCreator', () => { const expectedUser = { ...user, - currentGQLSession: JSON.stringify(user.currentGQLSession), - currentRESTSession: JSON.stringify(user.currentRESTSession), + currentGQLSession: null, + currentRESTSession: null, }; expect(result).toEqualRight(expectedUser); diff --git a/packages/hoppscotch-backend/src/published-docs/published-docs.service.ts b/packages/hoppscotch-backend/src/published-docs/published-docs.service.ts index d84e2399b7b..14b1da51b8e 100644 --- a/packages/hoppscotch-backend/src/published-docs/published-docs.service.ts +++ b/packages/hoppscotch-backend/src/published-docs/published-docs.service.ts @@ -253,8 +253,8 @@ export class PublishedDocsService { const creator = user ? { ...user, - currentGQLSession: JSON.stringify(user.currentGQLSession), - currentRESTSession: JSON.stringify(user.currentRESTSession), + currentGQLSession: null, + currentRESTSession: null, } : null; diff --git a/packages/hoppscotch-backend/src/team-invitation/team-invitation.resolver.ts b/packages/hoppscotch-backend/src/team-invitation/team-invitation.resolver.ts index b9f21a8b232..5520cec8809 100644 --- a/packages/hoppscotch-backend/src/team-invitation/team-invitation.resolver.ts +++ b/packages/hoppscotch-backend/src/team-invitation/team-invitation.resolver.ts @@ -65,8 +65,8 @@ export class TeamInvitationResolver { return { ...user.value, - currentGQLSession: JSON.stringify(user.value.currentGQLSession), - currentRESTSession: JSON.stringify(user.value.currentRESTSession), + currentGQLSession: null, + currentRESTSession: null, }; } diff --git a/packages/hoppscotch-backend/src/team/team-member.resolver.ts b/packages/hoppscotch-backend/src/team/team-member.resolver.ts index c8b9b22070f..595c9de801f 100644 --- a/packages/hoppscotch-backend/src/team/team-member.resolver.ts +++ b/packages/hoppscotch-backend/src/team/team-member.resolver.ts @@ -17,8 +17,8 @@ export class TeamMemberResolver { return { ...member.value, - currentRESTSession: JSON.stringify(member.value.currentRESTSession), - currentGQLSession: JSON.stringify(member.value.currentGQLSession), + currentRESTSession: null, + currentGQLSession: null, }; } } diff --git a/packages/hoppscotch-backend/src/user-environment/user.resolver.ts b/packages/hoppscotch-backend/src/user-environment/user.resolver.ts index c1d8d31d4ec..d55637f3a3c 100644 --- a/packages/hoppscotch-backend/src/user-environment/user.resolver.ts +++ b/packages/hoppscotch-backend/src/user-environment/user.resolver.ts @@ -1,9 +1,13 @@ import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { UseGuards } from '@nestjs/common'; import { User } from 'src/user/user.model'; import { UserEnvironment } from './user-environments.model'; import { UserEnvironmentsService } from './user-environments.service'; import * as E from 'fp-ts/Either'; import { throwErr } from '../utils'; +import { GqlAuthGuard } from '../guards/gql-auth.guard'; +import { GqlUser } from '../decorators/gql-user.decorator'; +import { USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS } from '../errors'; @Resolver(() => User) export class UserEnvsUserResolver { @@ -12,16 +16,26 @@ export class UserEnvsUserResolver { @ResolveField(() => [UserEnvironment], { description: 'Returns a list of users personal environments', }) - async environments(@Parent() user: User): Promise { + @UseGuards(GqlAuthGuard) + async environments( + @Parent() user: User, + @GqlUser() requestingUser: User, + ): Promise { + if (requestingUser?.uid !== user.uid) return []; return await this.userEnvironmentsService.fetchUserEnvironments(user.uid); } @ResolveField(() => UserEnvironment, { description: 'Returns the users global environments', }) + @UseGuards(GqlAuthGuard) async globalEnvironments( @Parent() user: User, + @GqlUser() requestingUser: User, ): Promise { + if (requestingUser?.uid !== user.uid) { + throwErr(USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS); + } const userEnvironment = await this.userEnvironmentsService.fetchUserGlobalEnvironment(user.uid); if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); diff --git a/packages/hoppscotch-backend/src/user-history/user-history.service.spec.ts b/packages/hoppscotch-backend/src/user-history/user-history.service.spec.ts index c9ccba49ec3..bf27ff43ce1 100644 --- a/packages/hoppscotch-backend/src/user-history/user-history.service.spec.ts +++ b/packages/hoppscotch-backend/src/user-history/user-history.service.spec.ts @@ -369,6 +369,53 @@ describe('UserHistoryService', () => { userHistory, ); }); + test('Should scope the lookup and the update to the requesting user (ownership enforcement)', async () => { + const executedOn = new Date(); + + mockPrisma.userHistory.findFirst.mockResolvedValueOnce({ + userUid: 'abc', + id: '1', + request: [{}], + responseMetadata: [{}], + reqType: ReqType.REST, + executedOn, + isStarred: false, + }); + mockPrisma.userHistory.update.mockResolvedValueOnce({ + userUid: 'abc', + id: '1', + request: [{}], + responseMetadata: [{}], + reqType: ReqType.REST, + executedOn, + isStarred: true, + }); + + await userHistoryService.toggleHistoryStarStatus('abc', '1'); + + expect(mockPrisma.userHistory.findFirst).toHaveBeenCalledWith({ + where: { id: '1', userUid: 'abc' }, + }); + expect(mockPrisma.userHistory.update).toHaveBeenCalledWith({ + where: { id: '1', userUid: 'abc' }, + data: { isStarred: true }, + }); + }); + test('Should resolve left when the history entry is not owned by the requesting user', async () => { + // Scoped lookup finds nothing because the entry belongs to another user + mockPrisma.userHistory.findFirst.mockResolvedValueOnce(null); + + const result = await userHistoryService.toggleHistoryStarStatus( + 'attacker', + '1', + ); + + expect(result).toEqualLeft(USER_HISTORY_NOT_FOUND); + // The lookup must be scoped to the requesting user's uid + expect(mockPrisma.userHistory.findFirst).toHaveBeenCalledWith({ + where: { id: '1', userUid: 'attacker' }, + }); + }); }); describe('removeRequestFromHistory', () => { test('Should resolve right and delete request from users history', async () => { @@ -433,6 +480,38 @@ describe('UserHistoryService', () => { userHistory, ); }); + test('Should scope the delete to the requesting user (ownership enforcement)', async () => { + mockPrisma.userHistory.delete.mockResolvedValueOnce({ + userUid: 'abc', + id: '1', + request: [{}], + responseMetadata: [{}], + reqType: ReqType.REST, + executedOn: date, + isStarred: false, + }); + + await userHistoryService.removeRequestFromHistory('abc', '1'); + + expect(mockPrisma.userHistory.delete).toHaveBeenCalledWith({ + where: { id: '1', userUid: 'abc' }, + }); + }); + test('Should resolve left when deleting a history entry not owned by the requesting user', async () => { + // Prisma throws when no row matches the user-scoped where clause + mockPrisma.userHistory.delete.mockRejectedValueOnce(new Error('P2025')); + + const result = await userHistoryService.removeRequestFromHistory( + 'attacker', + '1', + ); + + expect(result).toEqualLeft(USER_HISTORY_NOT_FOUND); + // The delete must be scoped to the requesting user's uid + expect(mockPrisma.userHistory.delete).toHaveBeenCalledWith({ + where: { id: '1', userUid: 'attacker' }, + }); + }); }); describe('deleteAllUserHistory', () => { test('Should resolve right and delete all user REST history for a request type', async () => { diff --git a/packages/hoppscotch-backend/src/user-history/user-history.service.ts b/packages/hoppscotch-backend/src/user-history/user-history.service.ts index 19d65ae4f12..c16978db655 100644 --- a/packages/hoppscotch-backend/src/user-history/user-history.service.ts +++ b/packages/hoppscotch-backend/src/user-history/user-history.service.ts @@ -99,7 +99,7 @@ export class UserHistoryService { * @returns an Either of updated `UserHistory` or Error */ async toggleHistoryStarStatus(uid: string, id: string) { - const userHistory = await this.fetchUserHistoryByID(id); + const userHistory = await this.fetchUserHistoryByID(id, uid); if (O.isNone(userHistory)) { return E.left(USER_HISTORY_NOT_FOUND); } @@ -108,6 +108,7 @@ export class UserHistoryService { const updatedHistory = await this.prisma.userHistory.update({ where: { id: id, + userUid: uid, }, data: { isStarred: !userHistory.value.isStarred, @@ -142,6 +143,7 @@ export class UserHistoryService { const delUserHistory = await this.prisma.userHistory.delete({ where: { id: id, + userUid: uid, }, }); @@ -205,14 +207,16 @@ export class UserHistoryService { } /** - * Fetch a user history based on history ID. + * Fetch a user history based on history ID, scoped to its owner. * @param id User History ID - * @returns an `UserHistory` object + * @param uid UID of the user the history entry must belong to + * @returns an `UserHistory` object owned by the given user, or `O.none` */ - async fetchUserHistoryByID(id: string) { + async fetchUserHistoryByID(id: string, uid: string) { const userHistory = await this.prisma.userHistory.findFirst({ where: { id: id, + userUid: uid, }, }); if (userHistory == null) return O.none; diff --git a/packages/hoppscotch-backend/src/user-history/user.resolver.ts b/packages/hoppscotch-backend/src/user-history/user.resolver.ts index b6758d159ae..3c46481432c 100644 --- a/packages/hoppscotch-backend/src/user-history/user.resolver.ts +++ b/packages/hoppscotch-backend/src/user-history/user.resolver.ts @@ -1,9 +1,12 @@ import { Args, Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { UseGuards } from '@nestjs/common'; import { User } from '../user/user.model'; import { UserHistoryService } from './user-history.service'; import { UserHistory } from './user-history.model'; import { ReqType } from 'src/types/RequestTypes'; import { PaginationArgs } from '../types/input-types.args'; +import { GqlAuthGuard } from '../guards/gql-auth.guard'; +import { GqlUser } from '../decorators/gql-user.decorator'; @Resolver(() => User) export class UserHistoryUserResolver { @@ -12,10 +15,13 @@ export class UserHistoryUserResolver { @ResolveField(() => [UserHistory], { description: 'Returns a users REST history', }) + @UseGuards(GqlAuthGuard) async RESTHistory( @Parent() user: User, + @GqlUser() requestingUser: User, @Args() args: PaginationArgs, ): Promise { + if (requestingUser?.uid !== user.uid) return []; return await this.userHistoryService.fetchUserHistory( user.uid, args.take, @@ -25,10 +31,13 @@ export class UserHistoryUserResolver { @ResolveField(() => [UserHistory], { description: 'Returns a users GraphQL history', }) + @UseGuards(GqlAuthGuard) async GQLHistory( @Parent() user: User, + @GqlUser() requestingUser: User, @Args() args: PaginationArgs, ): Promise { + if (requestingUser?.uid !== user.uid) return []; return await this.userHistoryService.fetchUserHistory( user.uid, args.take, diff --git a/packages/hoppscotch-backend/src/user-settings/user.resolver.ts b/packages/hoppscotch-backend/src/user-settings/user.resolver.ts index b5f63de6e52..d191e03cfde 100644 --- a/packages/hoppscotch-backend/src/user-settings/user.resolver.ts +++ b/packages/hoppscotch-backend/src/user-settings/user.resolver.ts @@ -1,9 +1,13 @@ import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { UseGuards } from '@nestjs/common'; import { User } from 'src/user/user.model'; import { UserSettings } from './user-settings.model'; import { UserSettingsService } from './user-settings.service'; import * as E from 'fp-ts/Either'; import { throwErr } from 'src/utils'; +import { GqlAuthGuard } from '../guards/gql-auth.guard'; +import { GqlUser } from '../decorators/gql-user.decorator'; +import { USER_SETTINGS_NOT_FOUND } from '../errors'; @Resolver(() => User) export class UserSettingsUserResolver { @@ -12,7 +16,9 @@ export class UserSettingsUserResolver { @ResolveField(() => UserSettings, { description: 'Returns user settings', }) - async settings(@Parent() user: User) { + @UseGuards(GqlAuthGuard) + async settings(@Parent() user: User, @GqlUser() requestingUser: User) { + if (requestingUser?.uid !== user.uid) throwErr(USER_SETTINGS_NOT_FOUND); const userSettings = await this.userSettingsService.fetchUserSettings(user); if (E.isLeft(userSettings)) throwErr(userSettings.left); From 73a88c82b1b2cada26cc4b2bc095b54554242239 Mon Sep 17 00:00:00 2001 From: Mir Arif Hasan Date: Thu, 11 Jun 2026 16:08:36 +0600 Subject: [PATCH 03/23] fix(backend): reject path/query/fragment in SMTP URL validation (GHSA-v7q6-r45w-2c6r) (#6413) * fix(backend): reject path/query/fragment in SMTP URL validation * refactor: fix ai feedbacks * fix(backend): harden SMTP URL validation against parser differentials --- packages/hoppscotch-backend/src/utils.ts | 25 ++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/hoppscotch-backend/src/utils.ts b/packages/hoppscotch-backend/src/utils.ts index 62d3f77ef85..f2c0901eeda 100644 --- a/packages/hoppscotch-backend/src/utils.ts +++ b/packages/hoppscotch-backend/src/utils.ts @@ -205,10 +205,27 @@ export const validateSMTPUrl = (url: string) => { if (!url || url.length === 0) return false; - const regex = - /^(smtp|smtps):\/\/(?:([^:]+):([^@]+)@)?((?!\.)[^:]+)(?::(\d+))?$/; - if (regex.test(url)) return true; - return false; + if (/[\s\x00-\x1f\x7f]/.test(url)) return false; + if (/[?#\\]/.test(url)) return false; + if (url.endsWith(':')) return false; + + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return false; + } + + // Only the SMTP schemes are permitted. + if (parsed.protocol !== 'smtp:' && parsed.protocol !== 'smtps:') return false; + if (parsed.pathname !== '' && parsed.pathname !== '/') return false; + if (parsed.search !== '' || parsed.hash !== '') return false; + + // A hostname is required and must not start with a dot. + if (!parsed.hostname || parsed.hostname.startsWith('.')) return false; + + // Port, when present, must be numeric (the URL parser already guarantees this). + return true; }; /** From afc98e99574fe8f11b5a558a21e19e78412a8f53 Mon Sep 17 00:00:00 2001 From: Nivedin <53208152+nivedin@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:35:23 +0530 Subject: [PATCH 04/23] feat(sh-admin): surface which config fields block saving (#6385) Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com> --- .../settings/AuthConfigurations.vue | 23 +- .../src/components/settings/AuthToken.vue | 46 ++- .../settings/OAuthProviderConfigurations.vue | 14 +- .../settings/ProxyURLConfiguration.vue | 34 +- .../src/components/settings/RateLimit.vue | 24 +- .../components/settings/SmtpConfiguration.vue | 51 ++- .../src/composables/useConfigHandler.ts | 158 +------- .../src/helpers/configs.ts | 377 +++++++++++++++++- .../src/pages/settings.vue | 135 +++++-- 9 files changed, 632 insertions(+), 230 deletions(-) diff --git a/packages/hoppscotch-sh-admin/src/components/settings/AuthConfigurations.vue b/packages/hoppscotch-sh-admin/src/components/settings/AuthConfigurations.vue index 6ba2318f053..560f4997182 100644 --- a/packages/hoppscotch-sh-admin/src/components/settings/AuthConfigurations.vue +++ b/packages/hoppscotch-sh-admin/src/components/settings/AuthConfigurations.vue @@ -13,6 +13,8 @@
- +
@@ -75,10 +82,20 @@ import { useVModel } from '@vueuse/core'; import { computed, onMounted, ref } from 'vue'; import { useI18n } from '~/composables/i18n'; -import { ServerConfigs } from '~/helpers/configs'; +import { + ConfigSubTab, + ServerConfigs, + tabHasConfigIssue, + useConfigValidation, +} from '~/helpers/configs'; const t = useI18n(); +const { configValidationIssues } = useConfigValidation(); + +const subTabHasError = (subTab: ConfigSubTab) => + tabHasConfigIssue(configValidationIssues.value, 'auth', subTab); + const props = defineProps<{ config: ServerConfigs; }>(); @@ -95,7 +112,7 @@ const workingConfigs = useVModel(props, 'config', emit); // Check if SMTP is activated but not saved yet. Used to track if SMTP was enabled after the last save. const isSMTPActivated = computed( - () => workingConfigs.value?.mailConfigs.enabled ?? false + () => workingConfigs.value?.mailConfigs.enabled ?? false, ); // Check if Email authentication is enabled diff --git a/packages/hoppscotch-sh-admin/src/components/settings/AuthToken.vue b/packages/hoppscotch-sh-admin/src/components/settings/AuthToken.vue index 3749bd47106..59bd8aa5300 100644 --- a/packages/hoppscotch-sh-admin/src/components/settings/AuthToken.vue +++ b/packages/hoppscotch-sh-admin/src/components/settings/AuthToken.vue @@ -32,6 +32,9 @@ placeholder="e.g., your-secret-key" :autofocus="false" class="!my-2 !bg-primaryLight flex-1 border border-divider rounded" + :class="{ + '!border-red-500': isConfigFieldErrored('token', 'jwt_secret'), + }" input-styles="!border-0 " :type="isMasked('jwt_secret') ? 'password' : 'text'" > @@ -53,6 +56,11 @@ placeholder="e.g., 10 (salt complexity)" :autofocus="false" class="!my-2 !bg-primaryLight flex-1" + :input-styles=" + isConfigFieldErrored('token', 'token_salt_complexity') + ? '!border-red-500' + : '' + " type="number" @update:model-value=" validateNumberValue( @@ -70,6 +78,11 @@ placeholder="e.g., 3 (in hour)" :autofocus="false" class="!my-2 !bg-primaryLight flex-1" + :input-styles=" + isConfigFieldErrored('token', 'magic_link_token_validity') + ? '!border-red-500' + : '' + " type="number" @update:model-value=" validateNumberValue( @@ -87,6 +100,11 @@ placeholder="e.g., 604800000 (in milliseconds)" :autofocus="false" class="!my-2 !bg-primaryLight flex-1" + :input-styles=" + isConfigFieldErrored('token', 'refresh_token_validity') + ? '!border-red-500' + : '' + " type="number" @update:model-value=" validateNumberValue( @@ -104,6 +122,11 @@ placeholder="e.g., 86400000 (in milliseconds)" :autofocus="false" class="!my-2 !bg-primaryLight flex-1" + :input-styles=" + isConfigFieldErrored('token', 'access_token_validity') + ? '!border-red-500' + : '' + " type="number" @update:model-value=" validateNumberValue( @@ -122,6 +145,10 @@ :autofocus="false" input-styles="!border-0 " class="!my-2 !bg-primaryLight flex-1 border border-divider rounded" + :class="{ + '!border-red-500': + isConfigFieldErrored('token', 'session_secret'), + }" :type="isMasked('session_secret') ? 'password' : 'text'" >