From cd10a32919c491a2687b3f31b620ef1fc297ca43 Mon Sep 17 00:00:00 2001 From: g-tejas Date: Wed, 3 Jul 2024 22:04:57 +0800 Subject: [PATCH 01/16] feat: add pdf attachments for encrypt forms --- .../edit-fieldtype/EditEmail/EditEmail.tsx | 8 +------- src/app/models/field/__tests__/emailField.spec.ts | 5 ++--- src/app/models/field/emailField.ts | 4 ++-- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditEmail/EditEmail.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditEmail/EditEmail.tsx index 99889f782f..442f5569b6 100644 --- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditEmail/EditEmail.tsx +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditEmail/EditEmail.tsx @@ -130,14 +130,9 @@ export const EditEmail = ({ field }: EditEmailProps): JSX.Element => { const { data: form } = useCreateTabForm() const isPdfResponseEnabled = useMemo( - () => form?.responseMode !== FormResponseMode.Encrypt, + () => form?.responseMode !== FormResponseMode.Multirespondent, [form], ) - const pdfResponseToggleDescription = useMemo(() => { - if (!isPdfResponseEnabled) { - return 'For security reasons, PDF responses are not included in email confirmations for Storage mode forms' - } - }, [isPdfResponseEnabled]) // email confirmation is not supported on MRF const isToggleEmailConfirmationDisabled = useMemo( @@ -239,7 +234,6 @@ export const EditEmail = ({ field }: EditEmailProps): JSX.Element => { diff --git a/src/app/models/field/__tests__/emailField.spec.ts b/src/app/models/field/__tests__/emailField.spec.ts index 398e1a62cd..f589279bee 100644 --- a/src/app/models/field/__tests__/emailField.spec.ts +++ b/src/app/models/field/__tests__/emailField.spec.ts @@ -44,7 +44,7 @@ describe('models.fields.emailField', () => { beforeEach(async () => await dbHandler.clearDatabase()) afterAll(async () => await dbHandler.closeDatabase()) - it('should set includeFormSummary to false on ResponseMode.Encrypt forms', async () => { + it('should set includeFormSummary to given value on ResponseMode.Encrypt forms', async () => { // Arrange const mockEmailField = { autoReplyOptions: { @@ -66,8 +66,7 @@ describe('models.fields.emailField', () => { const expected = merge(EMAIL_FIELD_DEFAULTS, mockEmailField, { _id: expect.anything(), autoReplyOptions: { - // Regardless, should be false since ResponseMode is Encrypt. - includeFormSummary: false, + includeFormSummary: true, }, }) expect(actual.field.toObject()).toEqual(expected) diff --git a/src/app/models/field/emailField.ts b/src/app/models/field/emailField.ts index 88018e8e3b..d66e44e555 100644 --- a/src/app/models/field/emailField.ts +++ b/src/app/models/field/emailField.ts @@ -31,8 +31,8 @@ const createEmailFieldSchema = (): Schema => { type: Boolean, default: false, set: function (this: IEmailFieldSchema, v: boolean) { - // Set to false if encrypt mode regardless of initial value. - return this.parent().responseMode === FormResponseMode.Encrypt + // Set to false if mrf mode regardless of initial value. + return this.parent().responseMode === FormResponseMode.Multirespondent ? false : v }, From ba36a2d3b1ed0a3844d7c63599c16d75b0c1d8b6 Mon Sep 17 00:00:00 2001 From: Ken Date: Thu, 20 Jun 2024 21:52:13 +0800 Subject: [PATCH 02/16] refactor: generalize user seen flag --- frontend/src/app/AdminNavBar/AdminNavBar.tsx | 29 ++++++------ .../feature-flags}/FeatureFlagService.ts | 2 +- .../src/features/feature-flags/queries.ts | 2 +- .../user}/UserService.ts | 11 +++-- frontend/src/features/user/constants.ts | 13 ++++++ frontend/src/features/user/mutations.ts | 14 +++--- frontend/src/features/user/queries.ts | 3 +- frontend/src/features/user/utils.ts | 24 ++++++++++ .../src/features/whats-new/utils/utils.ts | 13 ------ shared/types/user.ts | 17 +++++-- src/app/models/user.server.model.ts | 3 +- .../user/__tests__/user.controller.spec.ts | 46 +++++++++++++------ .../user/__tests__/user.service.spec.ts | 34 ++++++++------ src/app/modules/user/user.controller.ts | 23 +++++----- src/app/modules/user/user.middleware.ts | 5 +- src/app/modules/user/user.service.ts | 14 +++--- .../api/v3/user/__tests__/user.routes.spec.ts | 2 +- src/app/routes/api/v3/user/user.routes.ts | 15 ++++-- 18 files changed, 171 insertions(+), 99 deletions(-) rename frontend/src/{services => features/feature-flags}/FeatureFlagService.ts (76%) rename frontend/src/{services => features/user}/UserService.ts (84%) create mode 100644 frontend/src/features/user/constants.ts create mode 100644 frontend/src/features/user/utils.ts delete mode 100644 frontend/src/features/whats-new/utils/utils.ts diff --git a/frontend/src/app/AdminNavBar/AdminNavBar.tsx b/frontend/src/app/AdminNavBar/AdminNavBar.tsx index 9d46ecea2d..19ee937533 100644 --- a/frontend/src/app/AdminNavBar/AdminNavBar.tsx +++ b/frontend/src/app/AdminNavBar/AdminNavBar.tsx @@ -13,6 +13,8 @@ import { useDisclosure, } from '@chakra-ui/react' +import { SeenFlags } from '~shared/types' + import { BxsHelpCircle } from '~assets/icons/BxsHelpCircle' import { BxsRocket } from '~assets/icons/BxsRocket' import { ReactComponent as BrandMarkSvg } from '~assets/svgs/brand/brand-mark-colour.svg' @@ -31,12 +33,12 @@ import IconButton from '~components/IconButton' import Link from '~components/Link' import { AvatarMenu, AvatarMenuDivider } from '~templates/AvatarMenu/AvatarMenu' +import { SeenFlagsMapVersion } from '~features/user/constants' import { EmergencyContactModal } from '~features/user/emergency-contact/EmergencyContactModal' import { useUserMutations } from '~features/user/mutations' import { useUser } from '~features/user/queries' import { TransferOwnershipModal } from '~features/user/transfer-ownership/TransferOwnershipModal' -import { FEATURE_UPDATE_LIST } from '~features/whats-new/FeatureUpdateList' -import { getShowLatestFeatureUpdateNotification } from '~features/whats-new/utils/utils' +import { getShowFeatureFlagLastSeen } from '~features/user/utils' import { WhatsNewDrawer } from '~features/whats-new/WhatsNewDrawer' import Menu from '../../components/Menu' @@ -156,7 +158,7 @@ export interface AdminNavBarProps { export const AdminNavBar = ({ isMenuOpen }: AdminNavBarProps): JSX.Element => { const { user, isLoading: isUserLoading, removeQuery } = useUser() - const { updateLastSeenFeatureVersionMutation } = useUserMutations() + const { updateLastSeenFlagMutation } = useUserMutations() const whatsNewFeatureDrawerDisclosure = useDisclosure() @@ -199,26 +201,27 @@ export const AdminNavBar = ({ isMenuOpen }: AdminNavBarProps): JSX.Element => { const shouldShowFeatureUpdateNotification = useMemo(() => { if (isUserLoading || !user) return false - return getShowLatestFeatureUpdateNotification(user) + return getShowFeatureFlagLastSeen( + user, + SeenFlags.LastSeenFeatureUpdateVersion, + ) }, [isUserLoading, user]) const onWhatsNewDrawerOpen = useCallback(() => { if (isUserLoading || !user) return - // Update version if current user version is not set or is less than the latest version. - if ( - user.flags?.lastSeenFeatureUpdateVersion === undefined || - user.flags?.lastSeenFeatureUpdateVersion < FEATURE_UPDATE_LIST.version - ) { - updateLastSeenFeatureVersionMutation.mutateAsync( - FEATURE_UPDATE_LIST.version, - ) + if (shouldShowFeatureUpdateNotification) { + updateLastSeenFlagMutation.mutateAsync({ + version: SeenFlagsMapVersion.lastSeenFeatureUpdateVersion, + flag: SeenFlags.LastSeenFeatureUpdateVersion, + }) } whatsNewFeatureDrawerDisclosure.onOpen() }, [ isUserLoading, - updateLastSeenFeatureVersionMutation, + updateLastSeenFlagMutation, user, whatsNewFeatureDrawerDisclosure, + shouldShowFeatureUpdateNotification, ]) // Emergency contact modal appears after the rollout announcement modal diff --git a/frontend/src/services/FeatureFlagService.ts b/frontend/src/features/feature-flags/FeatureFlagService.ts similarity index 76% rename from frontend/src/services/FeatureFlagService.ts rename to frontend/src/features/feature-flags/FeatureFlagService.ts index 9ce5c32cd1..10fb3b7c04 100644 --- a/frontend/src/services/FeatureFlagService.ts +++ b/frontend/src/features/feature-flags/FeatureFlagService.ts @@ -1,4 +1,4 @@ -import { ApiService } from './ApiService' +import { ApiService } from '../../services/ApiService' export const getEnabledFeatureFlags = async (): Promise> => { return ApiService.get('/feature-flags/enabled').then( diff --git a/frontend/src/features/feature-flags/queries.ts b/frontend/src/features/feature-flags/queries.ts index 29784b512c..6fac736519 100644 --- a/frontend/src/features/feature-flags/queries.ts +++ b/frontend/src/features/feature-flags/queries.ts @@ -1,6 +1,6 @@ import { useQuery, UseQueryResult } from 'react-query' -import { getEnabledFeatureFlags } from '~services/FeatureFlagService' +import { getEnabledFeatureFlags } from '~features/feature-flags/FeatureFlagService' export const featureFlagsKeys = { base: ['feature-flags'] as const, diff --git a/frontend/src/services/UserService.ts b/frontend/src/features/user/UserService.ts similarity index 84% rename from frontend/src/services/UserService.ts rename to frontend/src/features/user/UserService.ts index 1749da5529..925f3dc568 100644 --- a/frontend/src/services/UserService.ts +++ b/frontend/src/features/user/UserService.ts @@ -1,11 +1,12 @@ import { SendUserContactOtpDto, TransferOwnershipRequestDto, + UpdateUserLastSeenFlagDto, UserDto, VerifyUserContactOtpDto, } from '~shared/types/user' -import { ApiService } from './ApiService' +import { ApiService } from '../../services/ApiService' const ADMIN_FORM_ENDPOINT = '/admin/forms' const USER_ENDPOINT = '/user' @@ -34,12 +35,12 @@ export const verifyUserContactOtp = ( ).then(({ data }) => data) } -export const updateUserLastSeenFeatureUpdateVersion = async ( - version: number, +export const updateUserLastSeenFlagVersion = async ( + params: UpdateUserLastSeenFlagDto, ): Promise => { return ApiService.post( - `${USER_ENDPOINT}/flag/new-features-last-seen`, - { version }, + `${USER_ENDPOINT}/flag/last-seen`, + params, ).then(({ data }) => data) } diff --git a/frontend/src/features/user/constants.ts b/frontend/src/features/user/constants.ts new file mode 100644 index 0000000000..25fe0146f1 --- /dev/null +++ b/frontend/src/features/user/constants.ts @@ -0,0 +1,13 @@ +import { SeenFlags } from '~shared/types' + +import { FEATURE_UPDATE_LIST } from '~features/whats-new/FeatureUpdateList' + +const LegacySeenFlags = { + [SeenFlags.LastSeenFeatureUpdateVersion]: FEATURE_UPDATE_LIST.version, +} + +export const SeenFlagsMapVersion: { [key in SeenFlags]: number } = { + ...LegacySeenFlags, + [SeenFlags.SettingsEmailNotification]: 0, // stub + [SeenFlags.CreateBuilderMrfWorkflow]: 0, // stub +} diff --git a/frontend/src/features/user/mutations.ts b/frontend/src/features/user/mutations.ts index 7354e12255..c6b29a73ea 100644 --- a/frontend/src/features/user/mutations.ts +++ b/frontend/src/features/user/mutations.ts @@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from 'react-query' import { SendUserContactOtpDto, TransferOwnershipRequestDto, + UpdateUserLastSeenFlagDto, UserDto, VerifyUserContactOtpDto, } from '~shared/types/user' @@ -10,12 +11,13 @@ import { import { ApiError } from '~typings/core' import { useToast } from '~hooks/useToast' + import { generateUserContactOtp, transferOwnership, - updateUserLastSeenFeatureUpdateVersion, + updateUserLastSeenFlagVersion, verifyUserContactOtp, -} from '~services/UserService' +} from '~features/user/UserService' import { userKeys } from './queries' @@ -43,11 +45,11 @@ export const useUserMutations = () => { }, }) - const updateLastSeenFeatureVersionMutation = useMutation< + const updateLastSeenFlagMutation = useMutation< UserDto, ApiError, - number - >((version: number) => updateUserLastSeenFeatureUpdateVersion(version), { + UpdateUserLastSeenFlagDto + >((params) => updateUserLastSeenFlagVersion(params), { onSuccess: (newData) => { queryClient.setQueryData(userKeys.base, newData) }, @@ -72,7 +74,7 @@ export const useUserMutations = () => { return { generateOtpMutation, verifyOtpMutation, - updateLastSeenFeatureVersionMutation, + updateLastSeenFlagMutation, transferOwnershipMutation, } } diff --git a/frontend/src/features/user/queries.ts b/frontend/src/features/user/queries.ts index 5d59ce1e97..b9c2356c1e 100644 --- a/frontend/src/features/user/queries.ts +++ b/frontend/src/features/user/queries.ts @@ -6,7 +6,8 @@ import { UserDto } from '~shared/types/user' import { LOGGED_IN_KEY } from '~constants/localStorage' import { useLocalStorage } from '~hooks/useLocalStorage' import { HttpError } from '~services/ApiService' -import { fetchUser } from '~services/UserService' + +import { fetchUser } from '~features/user/UserService' export const userKeys = { base: ['user'] as const, diff --git a/frontend/src/features/user/utils.ts b/frontend/src/features/user/utils.ts new file mode 100644 index 0000000000..7783607789 --- /dev/null +++ b/frontend/src/features/user/utils.ts @@ -0,0 +1,24 @@ +import { SeenFlags, UserDto } from '~shared/types' + +import { SeenFlagsMapVersion } from './constants' + +/** + * Returns whether the user should see the feature flag. + * @param user The user to check. + * @param flag The flag to check. + * @returns Boolean indicating whether the user should see the flag. + */ + +export const getShowFeatureFlagLastSeen = ( + user: UserDto | undefined, + flag: SeenFlags, +): boolean => { + const since = SeenFlagsMapVersion[flag] + const flagValue = user?.flags?.[flag] + if (flagValue == null) { + // If the flag is not set, failover as user has seen the flag. + return true + } + + return flagValue < since +} diff --git a/frontend/src/features/whats-new/utils/utils.ts b/frontend/src/features/whats-new/utils/utils.ts deleted file mode 100644 index 93b522555e..0000000000 --- a/frontend/src/features/whats-new/utils/utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { UserDto } from '~shared/types' - -import { FEATURE_UPDATE_LIST } from '../FeatureUpdateList' - -export const getShowLatestFeatureUpdateNotification = ( - user: UserDto | undefined, -): boolean => { - if (user?.flags?.lastSeenFeatureUpdateVersion === undefined) { - return true - } - - return user.flags.lastSeenFeatureUpdateVersion < FEATURE_UPDATE_LIST.version -} diff --git a/shared/types/user.ts b/shared/types/user.ts index f47e122e51..eebf900f97 100644 --- a/shared/types/user.ts +++ b/shared/types/user.ts @@ -1,10 +1,15 @@ import { z } from 'zod' -import type { Opaque } from 'type-fest' +import type { Tagged } from 'type-fest' import { DateString } from './generic' import { AgencyBase, AgencyDto, PublicAgencyDto } from './agency' +export type UserId = Tagged -export type UserId = Opaque +export enum SeenFlags { + LastSeenFeatureUpdateVersion = 'lastSeenFeatureUpdateVersion', + SettingsEmailNotification = 'settingsEmailNotification', + CreateBuilderMrfWorkflow = 'createBuilderMrfWorkflow', +} // Base used for being referenced by schema/model in the backend. // Note the lack of typing of _id. @@ -18,9 +23,7 @@ export const UserBase = z.object({ postmanSms: z.boolean().optional(), }) .optional(), - flags: z - .object({ lastSeenFeatureUpdateVersion: z.number().optional() }) - .optional(), + flags: z.map(z.nativeEnum(SeenFlags), z.number()).optional(), created: z.date(), lastAccessed: z.date().optional(), updatedAt: z.date(), @@ -82,3 +85,7 @@ export type TransferOwnershipResponseDto = { formIds: string[] error: string } +export type UpdateUserLastSeenFlagDto = { + version: number + flag: SeenFlags +} diff --git a/src/app/models/user.server.model.ts b/src/app/models/user.server.model.ts index 6c91b6314c..c5cef65291 100644 --- a/src/app/models/user.server.model.ts +++ b/src/app/models/user.server.model.ts @@ -77,7 +77,8 @@ const compileUserModel = (db: Mongoose) => { postmanSms: Boolean, }, flags: { - lastSeenFeatureUpdateVersion: Number, + type: Schema.Types.Map, // of SeenFlags + of: Number, }, apiToken: { select: false, diff --git a/src/app/modules/user/__tests__/user.controller.spec.ts b/src/app/modules/user/__tests__/user.controller.spec.ts index 4f0fa4fa47..c352059c58 100644 --- a/src/app/modules/user/__tests__/user.controller.spec.ts +++ b/src/app/modules/user/__tests__/user.controller.spec.ts @@ -1,6 +1,7 @@ import expressHandler from '__tests__/unit/backend/helpers/jest-express' import { StatusCodes } from 'http-status-codes' import { errAsync, okAsync } from 'neverthrow' +import { SeenFlags } from 'shared/types' import * as UserController from 'src/app/modules/user/user.controller' import { @@ -430,8 +431,9 @@ describe('user.controller', () => { }) }) - describe('handleUpdateUserLastSeenFeatureUpdateVersion', () => { + describe('handleUpdateUserLastSeenFlagVersion', () => { const MOCK_UPDATE_VERSION = 10 + const MOCK_FLAGS = SeenFlags.CreateBuilderMrfWorkflow const MOCK_REQ = expressHandler.mockRequest({ session: { user: { @@ -440,6 +442,7 @@ describe('user.controller', () => { }, body: { version: MOCK_UPDATE_VERSION, + flag: MOCK_FLAGS, }, }) @@ -453,12 +456,12 @@ describe('user.controller', () => { } // Mock all UserService calls to pass. - MockUserService.updateUserLastSeenFeatureUpdateVersion.mockReturnValueOnce( + MockUserService.updateUserLastSeenFlagVersion.mockReturnValueOnce( okAsync(mockPopulatedUser as IPopulatedUser), ) // Act - await UserController._handleUpdateUserLastSeenFeatureUpdateVersion( + await UserController._handleUpdateUserLastSeenFlagVersion( MOCK_REQ, mockRes, jest.fn(), @@ -467,8 +470,12 @@ describe('user.controller', () => { // Assert // Expect services to be called with correct arguments. expect( - MockUserService.updateUserLastSeenFeatureUpdateVersion, - ).toHaveBeenCalledWith(MOCK_REQ.session.user?._id, MOCK_UPDATE_VERSION) + MockUserService.updateUserLastSeenFlagVersion, + ).toHaveBeenCalledWith( + MOCK_REQ.session.user?._id, + MOCK_UPDATE_VERSION, + MOCK_FLAGS, + ) expect(mockRes.status).toHaveBeenCalledWith(200) expect(mockRes.json).toHaveBeenCalledWith(mockPopulatedUser) }) @@ -479,19 +486,20 @@ describe('user.controller', () => { session: {}, body: { version: MOCK_UPDATE_VERSION, + flag: MOCK_FLAGS, }, }) const mockRes = expressHandler.mockResponse() // Act - await UserController._handleUpdateUserLastSeenFeatureUpdateVersion( + await UserController._handleUpdateUserLastSeenFlagVersion( MOCK_REQ_WITH_NO_USER_ID_IN_SESSION, mockRes, jest.fn(), ) expect( - MockUserService.updateUserLastSeenFeatureUpdateVersion, + MockUserService.updateUserLastSeenFlagVersion, ).not.toHaveBeenCalled() expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.UNAUTHORIZED) expect(mockRes.json).toHaveBeenCalledWith(UNAUTHORIZED_USER_MESSAGE) @@ -503,20 +511,24 @@ describe('user.controller', () => { const expectedError = new MissingUserError('mock missing user error') // Mock all UserService calls to pass. - MockUserService.updateUserLastSeenFeatureUpdateVersion.mockReturnValueOnce( + MockUserService.updateUserLastSeenFlagVersion.mockReturnValueOnce( errAsync(expectedError), ) // Act - await UserController._handleUpdateUserLastSeenFeatureUpdateVersion( + await UserController._handleUpdateUserLastSeenFlagVersion( MOCK_REQ, mockRes, jest.fn(), ) expect( - MockUserService.updateUserLastSeenFeatureUpdateVersion, - ).toHaveBeenCalledWith(MOCK_REQ.session.user?._id, MOCK_UPDATE_VERSION) + MockUserService.updateUserLastSeenFlagVersion, + ).toHaveBeenCalledWith( + MOCK_REQ.session.user?._id, + MOCK_UPDATE_VERSION, + MOCK_FLAGS, + ) expect(mockRes.status).toHaveBeenCalledWith( StatusCodes.UNPROCESSABLE_ENTITY, ) @@ -529,20 +541,24 @@ describe('user.controller', () => { const expectedError = new DatabaseError('mock error') // Mock all UserService calls to pass. - MockUserService.updateUserLastSeenFeatureUpdateVersion.mockReturnValueOnce( + MockUserService.updateUserLastSeenFlagVersion.mockReturnValueOnce( errAsync(expectedError), ) // Act - await UserController._handleUpdateUserLastSeenFeatureUpdateVersion( + await UserController._handleUpdateUserLastSeenFlagVersion( MOCK_REQ, mockRes, jest.fn(), ) expect( - MockUserService.updateUserLastSeenFeatureUpdateVersion, - ).toHaveBeenCalledWith(MOCK_REQ.session.user?._id, MOCK_UPDATE_VERSION) + MockUserService.updateUserLastSeenFlagVersion, + ).toHaveBeenCalledWith( + MOCK_REQ.session.user?._id, + MOCK_UPDATE_VERSION, + MOCK_FLAGS, + ) expect(mockRes.status).toHaveBeenCalledWith( StatusCodes.INTERNAL_SERVER_ERROR, ) diff --git a/src/app/modules/user/__tests__/user.service.spec.ts b/src/app/modules/user/__tests__/user.service.spec.ts index 1c43636987..9be0c546de 100644 --- a/src/app/modules/user/__tests__/user.service.spec.ts +++ b/src/app/modules/user/__tests__/user.service.spec.ts @@ -4,6 +4,7 @@ import { zipWith } from 'lodash' import MockDate from 'mockdate' import mongoose, { LeanDocument, Query } from 'mongoose' import { errAsync, okAsync } from 'neverthrow' +import { SeenFlags } from 'shared/types' import getAdminVerificationModel from 'src/app/models/admin_verification.server.model' import getUserModel from 'src/app/models/user.server.model' @@ -310,28 +311,31 @@ describe('user.service', () => { }) }) - describe('updateUserLastSeenFeatureUpdateVersion', () => { + describe('updateUserLastSeenFlagVersion', () => { const MOCK_FEATURE_VERSION = 10 + const MOCK_FEATURE_FLAG = SeenFlags.CreateBuilderMrfWorkflow it('should update user successfully', async () => { const user = await dbHandler.insertUser({ agencyId: defaultAgency._id, - mailName: 'updateUserLastSeenFeatureUpdateVersion', + mailName: 'updateUserLastSeenFlagVersion', }) - expect(user.flags?.lastSeenFeatureUpdateVersion).toBeUndefined() + expect(user.flags?.get(MOCK_FEATURE_FLAG)).toBeUndefined() - const actualResult = - await UserService.updateUserLastSeenFeatureUpdateVersion( - user._id, - MOCK_FEATURE_VERSION, - ) + const actualResult = await UserService.updateUserLastSeenFlagVersion( + user._id, + MOCK_FEATURE_VERSION, + MOCK_FEATURE_FLAG, + ) const updatedUser = await UserService.getPopulatedUserById(user._id) expect(actualResult.isOk()).toEqual(true) expect( - updatedUser._unsafeUnwrap()?.toObject().flags - ?.lastSeenFeatureUpdateVersion, + updatedUser + ._unsafeUnwrap() + ?.toObject() + .flags?.get(MOCK_FEATURE_FLAG), ).toEqual(MOCK_FEATURE_VERSION) }) @@ -340,11 +344,11 @@ describe('user.service', () => { const invalidUserId = new ObjectID() // Act - const actualResult = - await UserService.updateUserLastSeenFeatureUpdateVersion( - invalidUserId, - MOCK_FEATURE_VERSION, - ) + const actualResult = await UserService.updateUserLastSeenFlagVersion( + invalidUserId, + MOCK_FEATURE_VERSION, + MOCK_FEATURE_FLAG, + ) expect(actualResult.isErr()).toEqual(true) expect(actualResult._unsafeUnwrapErr()).toBeInstanceOf(MissingUserError) }) diff --git a/src/app/modules/user/user.controller.ts b/src/app/modules/user/user.controller.ts index e74a98571c..9c576ca139 100644 --- a/src/app/modules/user/user.controller.ts +++ b/src/app/modules/user/user.controller.ts @@ -1,6 +1,7 @@ import { StatusCodes } from 'http-status-codes' import { + SeenFlags, SendUserContactOtpDto, VerifyUserContactOtpDto, } from '../../../../shared/types' @@ -15,13 +16,13 @@ import { UNAUTHORIZED_USER_MESSAGE } from './user.constant' import { validateContactOtpVerificationParams, validateContactSendOtpParams, - validateUpdateUserLastSeenFeatureUpdateVersion, + validateUpdateUserLastSeenFlagVersion, } from './user.middleware' import { createContactOtp, getPopulatedUserById, updateUserContact, - updateUserLastSeenFeatureUpdateVersion, + updateUserLastSeenFlagVersion, verifyContactOtp, } from './user.service' import { mapRouteError } from './user.utils' @@ -218,10 +219,10 @@ export const handleFetchUser: ControllerHandler = async (req, res) => { * @returns 422 when user id does not exist in the database * @returns 500 database errors occurs */ -export const _handleUpdateUserLastSeenFeatureUpdateVersion: ControllerHandler< +export const _handleUpdateUserLastSeenFlagVersion: ControllerHandler< unknown, IPopulatedUser | string, - { version: number } + { flag: SeenFlags; version: number } > = async (req, res) => { const sessionUserId = getUserIdFromSession(req.session) @@ -229,18 +230,18 @@ export const _handleUpdateUserLastSeenFeatureUpdateVersion: ControllerHandler< return res.status(StatusCodes.UNAUTHORIZED).json(UNAUTHORIZED_USER_MESSAGE) } - const { version } = req.body + const { flag, version } = req.body - return updateUserLastSeenFeatureUpdateVersion(sessionUserId, version) + return updateUserLastSeenFlagVersion(sessionUserId, version, flag) .map((updatedUser) => { return res.status(StatusCodes.OK).json(updatedUser) }) .mapErr((error) => { logger.error({ message: - 'Error occurred while updating user last seen feature update date', + 'Error occurred while updating user last seen flag update date', meta: { - action: 'handleUpdateUserLastSeenFeatureUpdate', + action: 'handleUpdateUserLastSeenFlagUpdate', userId: sessionUserId, }, error, @@ -251,7 +252,7 @@ export const _handleUpdateUserLastSeenFeatureUpdateVersion: ControllerHandler< }) } -export const handleUpdateUserLastSeenFeatureUpdateVersion = [ - validateUpdateUserLastSeenFeatureUpdateVersion, - _handleUpdateUserLastSeenFeatureUpdateVersion, +export const handleUpdateUserLastSeenFlagVersion = [ + validateUpdateUserLastSeenFlagVersion, + _handleUpdateUserLastSeenFlagVersion, ] as ControllerHandler[] diff --git a/src/app/modules/user/user.middleware.ts b/src/app/modules/user/user.middleware.ts index 747b960db1..700d6d924e 100644 --- a/src/app/modules/user/user.middleware.ts +++ b/src/app/modules/user/user.middleware.ts @@ -1,6 +1,8 @@ import JoiDate from '@joi/date' import { celebrate, Joi as BaseJoi, Segments } from 'celebrate' +import { SeenFlags } from '../../../../shared/types' + const Joi = BaseJoi.extend(JoiDate) as typeof BaseJoi /** @@ -26,8 +28,9 @@ export const validateContactOtpVerificationParams = celebrate({ }), }) -export const validateUpdateUserLastSeenFeatureUpdateVersion = celebrate({ +export const validateUpdateUserLastSeenFlagVersion = celebrate({ [Segments.BODY]: Joi.object({ version: Joi.number().required(), + flag: Joi.string().valid(...Object.values(SeenFlags)), }), }) diff --git a/src/app/modules/user/user.service.ts b/src/app/modules/user/user.service.ts index 1256a956e1..08b0ad9955 100644 --- a/src/app/modules/user/user.service.ts +++ b/src/app/modules/user/user.service.ts @@ -2,6 +2,7 @@ import mongoose from 'mongoose' import { errAsync, okAsync, ResultAsync } from 'neverthrow' import validator from 'validator' +import { SeenFlags } from '../../../../shared/types' import { IAdminVerificationDoc, IAgencySchema, @@ -164,25 +165,26 @@ export const updateUserContact = ( } /** - * Updates the user document with the userId with the given latest seen feature update date and + * Updates the user document with the userId with the given flag version and * returns the populated updated user. * @param userId the user id of the user document to update * @returns ok(true) if update was successful * @returns err(MissingUserError) if user document cannot be found * @returns err(DatabaseError) if any error occurs whilst querying the database */ -export const updateUserLastSeenFeatureUpdateVersion = ( +export const updateUserLastSeenFlagVersion = ( userId: IUserSchema['_id'], version: number, + flag: SeenFlags, ): ResultAsync => { // Retrieve user from database and - // update user's last seen feature update date attribute. + // update user's last seen feature version. return ResultAsync.fromPromise( UserModel.findByIdAndUpdate( userId, { $set: { - flags: { lastSeenFeatureUpdateVersion: version }, + [`flags.${flag}`]: version, }, }, { new: true }, @@ -195,8 +197,8 @@ export const updateUserLastSeenFeatureUpdateVersion = ( (error) => { logger.error({ message: - 'Database error when updating user last seen feature update version', - meta: { action: 'updateUserLastSeenFeatureUpdateVersion', userId }, + 'Database error when updating user last seen flag update version', + meta: { action: 'updateUserSeenFlagVersion', flag, userId }, error, }) diff --git a/src/app/routes/api/v3/user/__tests__/user.routes.spec.ts b/src/app/routes/api/v3/user/__tests__/user.routes.spec.ts index 79f7177548..c714d8410a 100644 --- a/src/app/routes/api/v3/user/__tests__/user.routes.spec.ts +++ b/src/app/routes/api/v3/user/__tests__/user.routes.spec.ts @@ -589,7 +589,7 @@ describe('user.routes', () => { const mockErrorString = 'Database goes boom' // Mock database error from service call. const retrieveUserSpy = jest - .spyOn(UserService, 'updateUserLastSeenFeatureUpdateVersion') + .spyOn(UserService, 'updateUserLastSeenFlagVersion') .mockReturnValueOnce(errAsync(new DatabaseError(mockErrorString))) // Act diff --git a/src/app/routes/api/v3/user/user.routes.ts b/src/app/routes/api/v3/user/user.routes.ts index b752babd15..0515743b54 100644 --- a/src/app/routes/api/v3/user/user.routes.ts +++ b/src/app/routes/api/v3/user/user.routes.ts @@ -54,17 +54,24 @@ UserRouter.post( ) /** - * Verify the contact verification one-time password (OTP) for the user as part - * of the contact verification process - * @route POST /user/flag/new-features-last-seen + * @route POST /user/flag/last-seen * @returns 200 when user last seen feature update updates sucessfully * @returns 401 if user is not currently logged in * @returns 422 when userId does not exist in the database * @returns 500 when database errors occurs */ +UserRouter.post( + '/flag/last-seen', + UserController.handleUpdateUserLastSeenFlagVersion, +) +/** + * @deprecated route + * Backwards compatibility for the old route + * Use /flag/last-seen instead + */ UserRouter.post( '/flag/new-features-last-seen', - UserController.handleUpdateUserLastSeenFeatureUpdateVersion, + UserController.handleUpdateUserLastSeenFlagVersion, ) export default UserRouter From d883952df327928e92d007c46fae427851a5dc7a Mon Sep 17 00:00:00 2001 From: Ken Date: Thu, 4 Jul 2024 16:49:09 +0800 Subject: [PATCH 03/16] handle old last seen route --- src/app/modules/user/user.controller.ts | 22 ++++++++++++++++++++++ src/app/modules/user/user.middleware.ts | 15 ++++++++++++++- src/app/routes/api/v3/user/user.routes.ts | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/app/modules/user/user.controller.ts b/src/app/modules/user/user.controller.ts index 9c576ca139..c72fddc1a2 100644 --- a/src/app/modules/user/user.controller.ts +++ b/src/app/modules/user/user.controller.ts @@ -17,6 +17,7 @@ import { validateContactOtpVerificationParams, validateContactSendOtpParams, validateUpdateUserLastSeenFlagVersion, + validateUpdateUserNewFeaturesLastSeenVersion, } from './user.middleware' import { createContactOtp, @@ -256,3 +257,24 @@ export const handleUpdateUserLastSeenFlagVersion = [ validateUpdateUserLastSeenFlagVersion, _handleUpdateUserLastSeenFlagVersion, ] as ControllerHandler[] + +const injectUserNewFeatureFlag: ControllerHandler< + unknown, + unknown, + { version: number; flag?: SeenFlags } +> = (req, res, next) => { + req.body.flag = SeenFlags.LastSeenFeatureUpdateVersion + return next() +} + +/** + * @deprecated handler + * Backwards compatibility for the old route + * Use handleUpdateUserLastSeenFlagVersion instead that contains flag params + */ +export const handleUpdateNewFeaturesUserLastSeenVersion = [ + validateUpdateUserNewFeaturesLastSeenVersion, + injectUserNewFeatureFlag, + validateUpdateUserLastSeenFlagVersion, + _handleUpdateUserLastSeenFlagVersion, +] as ControllerHandler[] diff --git a/src/app/modules/user/user.middleware.ts b/src/app/modules/user/user.middleware.ts index 700d6d924e..9e9ebdfd5d 100644 --- a/src/app/modules/user/user.middleware.ts +++ b/src/app/modules/user/user.middleware.ts @@ -31,6 +31,19 @@ export const validateContactOtpVerificationParams = celebrate({ export const validateUpdateUserLastSeenFlagVersion = celebrate({ [Segments.BODY]: Joi.object({ version: Joi.number().required(), - flag: Joi.string().valid(...Object.values(SeenFlags)), + flag: Joi.string() + .valid(...Object.values(SeenFlags)) + .required(), + }), +}) + +/** + * @deprecated validator + * Backwards compatibility for the old route + * use validateUpdateUserLastSeenFlagVersion instead + */ +export const validateUpdateUserNewFeaturesLastSeenVersion = celebrate({ + [Segments.BODY]: Joi.object({ + version: Joi.number().required(), }), }) diff --git a/src/app/routes/api/v3/user/user.routes.ts b/src/app/routes/api/v3/user/user.routes.ts index 0515743b54..9136b3fe64 100644 --- a/src/app/routes/api/v3/user/user.routes.ts +++ b/src/app/routes/api/v3/user/user.routes.ts @@ -71,7 +71,7 @@ UserRouter.post( */ UserRouter.post( '/flag/new-features-last-seen', - UserController.handleUpdateUserLastSeenFlagVersion, + UserController.handleUpdateNewFeaturesUserLastSeenVersion, ) export default UserRouter From a044b805076b6ad14b44fd7baf7c2d73ba73bddc Mon Sep 17 00:00:00 2001 From: Ken Date: Sun, 23 Jun 2024 17:28:15 +0800 Subject: [PATCH 04/16] add reddot to mrf workflow --- .../CreatePageSidebar/CreatePageSidebar.tsx | 34 +++++++++++++++---- .../CreatePageSidebar/DrawerTabIcon.tsx | 31 ++++++++++++----- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/frontend/src/features/admin-form/create/common/CreatePageSidebar/CreatePageSidebar.tsx b/frontend/src/features/admin-form/create/common/CreatePageSidebar/CreatePageSidebar.tsx index 01c6e8d080..99a681405b 100644 --- a/frontend/src/features/admin-form/create/common/CreatePageSidebar/CreatePageSidebar.tsx +++ b/frontend/src/features/admin-form/create/common/CreatePageSidebar/CreatePageSidebar.tsx @@ -1,8 +1,8 @@ -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' import { BiGitMerge, BiQuestionMark } from 'react-icons/bi' import { Divider, Stack } from '@chakra-ui/react' -import { FormResponseMode } from '~shared/types' +import { FormResponseMode, SeenFlags } from '~shared/types' import { MultiParty, PhHandsClapping } from '~assets/icons' import { BxsDockTop } from '~assets/icons/BxsDockTop' @@ -17,6 +17,10 @@ import { DrawerTabs, useCreatePageSidebar, } from '~features/admin-form/create/common/CreatePageSidebarContext/CreatePageSidebarContext' +import { SeenFlagsMapVersion } from '~features/user/constants' +import { useUserMutations } from '~features/user/mutations' +import { useUser } from '~features/user/queries' +import { getShowFeatureFlagLastSeen } from '~features/user/utils' import { isDirtySelector, @@ -34,6 +38,13 @@ export const CreatePageSidebar = (): JSX.Element | null => { const isMobile = useIsMobile() const { data } = useAdminForm() + const { user, isLoading: isUserLoading } = useUser() + const { updateLastSeenFlagMutation } = useUserMutations() + + const shouldShowMrfWorkflowReddot = useMemo(() => { + if (isUserLoading || !user) return false + return getShowFeatureFlagLastSeen(user, SeenFlags.CreateBuilderMrfWorkflow) + }, [isUserLoading, user]) const setFieldsToInactive = useFieldBuilderStore(setToInactiveSelector) const isDirty = useDirtyFieldStore(isDirtySelector) @@ -69,10 +80,20 @@ export const CreatePageSidebar = (): JSX.Element | null => { [handleEndpageClick, isDirty], ) - const handleDrawerWorkflowClick = useCallback( - () => handleWorkflowClick(isDirty), - [handleWorkflowClick, isDirty], - ) + const handleDrawerWorkflowClick = useCallback(() => { + handleWorkflowClick(isDirty) + if (shouldShowMrfWorkflowReddot) { + updateLastSeenFlagMutation.mutate({ + flag: SeenFlags.CreateBuilderMrfWorkflow, + version: SeenFlagsMapVersion.createBuilderMrfWorkflow, + }) + } + }, [ + handleWorkflowClick, + updateLastSeenFlagMutation, + shouldShowMrfWorkflowReddot, + isDirty, + ]) return ( { icon={} onClick={handleDrawerWorkflowClick} isActive={activeTab === DrawerTabs.Workflow} + showRedDot={shouldShowMrfWorkflowReddot} /> )} diff --git a/frontend/src/features/admin-form/create/common/CreatePageSidebar/DrawerTabIcon.tsx b/frontend/src/features/admin-form/create/common/CreatePageSidebar/DrawerTabIcon.tsx index 3a02d4153f..9fb300aa90 100644 --- a/frontend/src/features/admin-form/create/common/CreatePageSidebar/DrawerTabIcon.tsx +++ b/frontend/src/features/admin-form/create/common/CreatePageSidebar/DrawerTabIcon.tsx @@ -1,3 +1,6 @@ +import { GoPrimitiveDot } from 'react-icons/go' +import { Box, Icon } from '@chakra-ui/react' + import IconButton from '~components/IconButton' import Tooltip from '~components/Tooltip' @@ -7,6 +10,7 @@ interface DrawerTabIconProps { label: string isActive: boolean id?: string + showRedDot?: boolean } export const DrawerTabIcon = ({ @@ -15,17 +19,28 @@ export const DrawerTabIcon = ({ label, isActive, id, + showRedDot, }: DrawerTabIconProps): JSX.Element => { return ( - + + + {showRedDot ? ( + + ) : null} + ) } From 73689dd968638f398a7d9fa12a6769a75c2ab756 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jul 2024 20:25:07 +0800 Subject: [PATCH 05/16] fix(deps): bump uuid and @types/uuid (#7467) Bumps [uuid](https://github.com/uuidjs/uuid) and [@types/uuid](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/uuid). These dependencies needed to be updated together. Updates `uuid` from 9.0.1 to 10.0.0 - [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md) - [Commits](https://github.com/uuidjs/uuid/compare/v9.0.1...v10.0.0) Updates `@types/uuid` from 9.0.8 to 10.0.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/uuid) --- updated-dependencies: - dependency-name: uuid dependency-type: direct:production update-type: version-update:semver-major - dependency-name: "@types/uuid" dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 53 ++++++++++++++++++++++++++++++++++++++++------- package.json | 4 ++-- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ce28aa1c3..12120e1ec2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -104,7 +104,7 @@ "twilio": "~4.19.3", "uid-generator": "^2.0.0", "ulid": "^2.3.0", - "uuid": "^9.0.0", + "uuid": "^10.0.0", "uuid-by-string": "^4.0.0", "validator": "^13.12.0", "web-streams-polyfill": "^3.2.1", @@ -155,7 +155,7 @@ "@types/supertest": "^2.0.12", "@types/triple-beam": "^1.3.2", "@types/uid-generator": "^2.0.3", - "@types/uuid": "^9.0.8", + "@types/uuid": "^10.0.0", "@types/validator": "^13.12.0", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", @@ -365,6 +365,18 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@aws-sdk/client-cognito-identity": { "version": "3.433.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.433.0.tgz", @@ -6231,6 +6243,19 @@ "node": ">=8.0.0" } }, + "node_modules/@opengovsg/mockpass/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@opengovsg/myinfo-gov-client": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@opengovsg/myinfo-gov-client/-/myinfo-gov-client-4.1.2.tgz", @@ -9731,9 +9756,9 @@ "license": "MIT" }, "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", "dev": true }, "node_modules/@types/validator": { @@ -23258,6 +23283,18 @@ "version": "2.0.4", "license": "(MIT AND Zlib)" }, + "node_modules/node-jose/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/node-libs-browser": { "version": "2.2.1", "dev": true, @@ -29107,9 +29144,9 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/package.json b/package.json index 9ac80af3b6..552a0b6951 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,7 @@ "twilio": "~4.19.3", "uid-generator": "^2.0.0", "ulid": "^2.3.0", - "uuid": "^9.0.0", + "uuid": "^10.0.0", "uuid-by-string": "^4.0.0", "validator": "^13.12.0", "web-streams-polyfill": "^3.2.1", @@ -201,7 +201,7 @@ "@types/supertest": "^2.0.12", "@types/triple-beam": "^1.3.2", "@types/uid-generator": "^2.0.3", - "@types/uuid": "^9.0.8", + "@types/uuid": "^10.0.0", "@types/validator": "^13.12.0", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", From 5ff6864a1c9e6b22a198d092c71087adcac7ac63 Mon Sep 17 00:00:00 2001 From: Ken Lee Shu Ming Date: Thu, 4 Jul 2024 20:44:16 +0800 Subject: [PATCH 06/16] chore(privacy policy): update clause 6.3 (#7477) * Update PrivacyPolicyPage.tsx Updating clause 6.3 * fix: lint issues --------- Co-authored-by: Nitya <100560092+NityaMenon@users.noreply.github.com> --- .../src/pages/PrivacyPolicy/PrivacyPolicyPage.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/PrivacyPolicy/PrivacyPolicyPage.tsx b/frontend/src/pages/PrivacyPolicy/PrivacyPolicyPage.tsx index 820b6beda6..b7bca6ba71 100644 --- a/frontend/src/pages/PrivacyPolicy/PrivacyPolicyPage.tsx +++ b/frontend/src/pages/PrivacyPolicy/PrivacyPolicyPage.tsx @@ -226,10 +226,13 @@ export const PrivacyPolicyPage = (): JSX.Element => { isNumericMarker prependSequenceMarker="6." > - We will NOT share your personal data with entities which are - not Government Agencies, except where such sharing is - necessary for such entities to assist us in providing the - Service to you or for fulfilling any of the purposes herein. + We may share your personal data with non-Government Agency + entities that have been authorised to carry out specific + Government agency services. We will NOT share your personal + data with other non-Government Agency entities without your + consent, except where such sharing is necessary for + fulfilling any of the purposes herein, or complies with the + law. Date: Fri, 5 Jul 2024 11:11:27 +0800 Subject: [PATCH 07/16] feat: add reddot on email noti (#7463) * feat: add reddot on email noti * move reddot from email tab to settings navbar --- .../AdminFormNavbar/AdminFormNavbar.tsx | 32 +++++++++++++++++++ .../settings/SettingsEmailsPage.tsx | 2 +- .../admin-form/settings/SettingsPage.tsx | 16 ++++++++-- .../settings/components/SettingsTab.tsx | 18 ++++++++++- frontend/src/features/user/constants.ts | 4 +-- .../NavigationTabs/NavigationTab.tsx | 1 + shared/types/user.ts | 2 +- 7 files changed, 67 insertions(+), 8 deletions(-) diff --git a/frontend/src/features/admin-form/common/components/AdminFormNavbar/AdminFormNavbar.tsx b/frontend/src/features/admin-form/common/components/AdminFormNavbar/AdminFormNavbar.tsx index d5ae273f5f..6c3bbae6cd 100644 --- a/frontend/src/features/admin-form/common/components/AdminFormNavbar/AdminFormNavbar.tsx +++ b/frontend/src/features/admin-form/common/components/AdminFormNavbar/AdminFormNavbar.tsx @@ -5,6 +5,7 @@ import { BiShow, BiUserPlus, } from 'react-icons/bi' +import { GoPrimitiveDot } from 'react-icons/go' import { Link as ReactLink, useLocation } from 'react-router-dom' import { Box, @@ -18,6 +19,7 @@ import { Flex, Grid, GridItem, + Icon, Skeleton, Text, useBreakpointValue, @@ -25,6 +27,7 @@ import { } from '@chakra-ui/react' import format from 'date-fns/format' +import { SeenFlags } from '~shared/types' import { AdminFormDto } from '~shared/types/form/form' import { @@ -40,6 +43,11 @@ import IconButton from '~components/IconButton' import Tooltip from '~components/Tooltip' import { NavigationTab, NavigationTabList } from '~templates/NavigationTabs' +import { SeenFlagsMapVersion } from '~features/user/constants' +import { useUserMutations } from '~features/user/mutations' +import { useUser } from '~features/user/queries' +import { getShowFeatureFlagLastSeen } from '~features/user/utils' + import { AdminFormNavbarBreadcrumbs } from './AdminFormNavbarBreadcrumbs' export interface AdminFormNavbarProps { @@ -68,6 +76,13 @@ export const AdminFormNavbar = ({ const { isOpen, onClose, onOpen } = useDisclosure() const { pathname } = useLocation() + const { user, isLoading: isUserLoading } = useUser() + const { updateLastSeenFlagMutation } = useUserMutations() + const shouldShowSettingsReddot = useMemo(() => { + if (isUserLoading || !user) return false + return getShowFeatureFlagLastSeen(user, SeenFlags.SettingsNotification) + }, [isUserLoading, user]) + const tabResponsiveVariant = useBreakpointValue({ base: 'line-dark', xs: 'line-dark', @@ -171,8 +186,25 @@ export const AdminFormNavbar = ({ hidden={viewOnly} to={ADMINFORM_SETTINGS_SUBROUTE} isActive={checkTabActive(ADMINFORM_SETTINGS_SUBROUTE)} + onClick={() => { + if (shouldShowSettingsReddot) { + updateLastSeenFlagMutation.mutate({ + flag: SeenFlags.SettingsNotification, + version: SeenFlagsMapVersion[SeenFlags.SettingsNotification], + }) + } + }} > Settings + {shouldShowSettingsReddot ? ( + + ) : null} { // should render null if (!isEmailOrStorageMode) { - return false + return null } return diff --git a/frontend/src/features/admin-form/settings/SettingsPage.tsx b/frontend/src/features/admin-form/settings/SettingsPage.tsx index abf3a42262..06f31f1986 100644 --- a/frontend/src/features/admin-form/settings/SettingsPage.tsx +++ b/frontend/src/features/admin-form/settings/SettingsPage.tsx @@ -41,6 +41,7 @@ interface TabEntry { icon: IconType component: () => JSX.Element path: string + showRedDot?: boolean } export const SettingsPage = (): JSX.Element => { @@ -68,6 +69,7 @@ export const SettingsPage = (): JSX.Element => { icon: BiMailSend, component: SettingsEmailsPage, path: 'email-notifications', + showRedDot: true, } : null @@ -106,7 +108,7 @@ export const SettingsPage = (): JSX.Element => { ] return baseConfig.filter(isNonEmpty) - }, [settings]) + }, [settings?.responseMode]) const { ref, onMouseDown } = useDraggable() @@ -128,7 +130,10 @@ export const SettingsPage = (): JSX.Element => { py={{ base: '2.5rem', lg: '3.125rem' }} px={{ base: '1.5rem', md: '1.75rem', lg: '2rem' }} index={tabIndex === -1 ? 0 : tabIndex} - onChange={handleTabChange} + onChange={(index) => { + handleTabChange(index) + tabConfig[index] + }} > { mb="calc(0.5rem - 2px)" > {tabConfig.map((tab) => ( - + ))} diff --git a/frontend/src/features/admin-form/settings/components/SettingsTab.tsx b/frontend/src/features/admin-form/settings/components/SettingsTab.tsx index 128b6ba36e..3f70fd8447 100644 --- a/frontend/src/features/admin-form/settings/components/SettingsTab.tsx +++ b/frontend/src/features/admin-form/settings/components/SettingsTab.tsx @@ -1,17 +1,33 @@ import { As, Box, Icon, Tab } from '@chakra-ui/react' +import Badge from '~components/Badge' + export interface SettingsTabProps { label: string icon: As + showNewBadge?: boolean } -export const SettingsTab = ({ label, icon }: SettingsTabProps): JSX.Element => { +export const SettingsTab = ({ + label, + icon, + showNewBadge = false, +}: SettingsTabProps): JSX.Element => { return ( {label} + {showNewBadge ? ( + + New + + ) : null} ) } diff --git a/frontend/src/features/user/constants.ts b/frontend/src/features/user/constants.ts index 25fe0146f1..10876206f7 100644 --- a/frontend/src/features/user/constants.ts +++ b/frontend/src/features/user/constants.ts @@ -8,6 +8,6 @@ const LegacySeenFlags = { export const SeenFlagsMapVersion: { [key in SeenFlags]: number } = { ...LegacySeenFlags, - [SeenFlags.SettingsEmailNotification]: 0, // stub - [SeenFlags.CreateBuilderMrfWorkflow]: 0, // stub + [SeenFlags.SettingsNotification]: 0, + [SeenFlags.CreateBuilderMrfWorkflow]: 0, } diff --git a/frontend/src/templates/NavigationTabs/NavigationTab.tsx b/frontend/src/templates/NavigationTabs/NavigationTab.tsx index a949f7e806..dad73ab0aa 100644 --- a/frontend/src/templates/NavigationTabs/NavigationTab.tsx +++ b/frontend/src/templates/NavigationTabs/NavigationTab.tsx @@ -7,6 +7,7 @@ const Link = chakra(ReactLink) interface NavigationTabProps extends ComponentProps { isActive?: boolean isDisabled?: boolean + showReddot?: boolean } /** Must be nested inside NavigationTabList component, uses styles provided by that component. */ diff --git a/shared/types/user.ts b/shared/types/user.ts index eebf900f97..23e8c855e3 100644 --- a/shared/types/user.ts +++ b/shared/types/user.ts @@ -7,7 +7,7 @@ export type UserId = Tagged export enum SeenFlags { LastSeenFeatureUpdateVersion = 'lastSeenFeatureUpdateVersion', - SettingsEmailNotification = 'settingsEmailNotification', + SettingsNotification = 'settingsNotification', CreateBuilderMrfWorkflow = 'createBuilderMrfWorkflow', } From cc4c570d9b3d305e7549c502eba980e2c3cef582 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:07:02 +0800 Subject: [PATCH 08/16] chore(deps-dev): bump @types/jsonwebtoken from 8.5.9 to 9.0.6 (#7478) Bumps [@types/jsonwebtoken](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jsonwebtoken) from 8.5.9 to 9.0.6. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jsonwebtoken) --- updated-dependencies: - dependency-name: "@types/jsonwebtoken" dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 7 ++++--- package.json | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 12120e1ec2..a611dd02ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -140,7 +140,7 @@ "@types/jest": "^29.5.1", "@types/json-stringify-safe": "^5.0.3", "@types/jsonfile": "^6.1.1", - "@types/jsonwebtoken": "^8.5.9", + "@types/jsonwebtoken": "^9.0.6", "@types/jwk-to-pem": "^2.0.3", "@types/lodash": "^4.14.191", "@types/mongodb-uri": "^0.9.1", @@ -9530,9 +9530,10 @@ } }, "node_modules/@types/jsonwebtoken": { - "version": "8.5.9", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } diff --git a/package.json b/package.json index 552a0b6951..262a9361f9 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ "@types/jest": "^29.5.1", "@types/json-stringify-safe": "^5.0.3", "@types/jsonfile": "^6.1.1", - "@types/jsonwebtoken": "^8.5.9", + "@types/jsonwebtoken": "^9.0.6", "@types/jwk-to-pem": "^2.0.3", "@types/lodash": "^4.14.191", "@types/mongodb-uri": "^0.9.1", From 6f911ed6785b81bf71f15849d7dd160f5d1068e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:11:59 +0800 Subject: [PATCH 09/16] fix(deps): bump jsdom from 21.1.1 to 24.1.0 (#7345) Bumps [jsdom](https://github.com/jsdom/jsdom) from 21.1.1 to 24.1.0. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Changelog](https://github.com/jsdom/jsdom/blob/main/Changelog.md) - [Commits](https://github.com/jsdom/jsdom/compare/21.1.1...24.1.0) --- updated-dependencies: - dependency-name: jsdom dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 326 ++++++++++++++++++++++++---------------------- package.json | 2 +- 2 files changed, 170 insertions(+), 158 deletions(-) diff --git a/package-lock.json b/package-lock.json index a611dd02ec..5a97afc6a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,7 @@ "intl-tel-input": "~12.4.0", "ip": "^1.1.9", "jose": "^4.15.5", - "jsdom": "^21.1.1", + "jsdom": "^24.1.0", "json-stringify-safe": "^5.0.1", "JSONStream": "^1.3.5", "jsonwebtoken": "^9.0.2", @@ -9175,6 +9175,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, "engines": { "node": ">= 10" } @@ -10591,7 +10592,8 @@ "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true }, "node_modules/abbrev": { "version": "1.1.1", @@ -13447,6 +13449,49 @@ "node": ">= 14" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -17262,6 +17307,17 @@ "unix-dgram": "2.x" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-entities": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", @@ -20682,42 +20738,37 @@ "license": "MIT" }, "node_modules/jsdom": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-21.1.1.tgz", - "integrity": "sha512-Jjgdmw48RKcdAIQyUD1UdBh2ecH7VqwaXPN3ehoZN6MqgVbMn+lRm1aAT1AsdJRAJpwfa4IpwgzySn61h2qu3w==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz", + "integrity": "sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==", "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.8.2", - "acorn-globals": "^7.0.0", - "cssstyle": "^3.0.0", - "data-urls": "^4.0.0", + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", "decimal.js": "^10.4.3", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", + "nwsapi": "^2.2.10", "parse5": "^7.1.2", - "rrweb-cssom": "^0.6.0", + "rrweb-cssom": "^0.7.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.1", - "ws": "^8.13.0", - "xml-name-validator": "^4.0.0" + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.17.0", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^2.11.2" }, "peerDependenciesMeta": { "canvas": { @@ -20725,102 +20776,43 @@ } } }, - "node_modules/jsdom/node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/jsdom/node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "node_modules/jsdom/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, - "node_modules/jsdom/node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "debug": "^4.3.4" + }, "engines": { - "node": ">=0.4.0" + "node": ">= 14" } }, "node_modules/jsdom/node_modules/cssstyle": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", - "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", + "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", "dependencies": { "rrweb-cssom": "^0.6.0" }, "engines": { - "node": ">=14" - } - }, - "node_modules/jsdom/node_modules/data-urls": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", - "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", - "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/jsdom/node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/jsdom/node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } + "node_modules/jsdom/node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==" }, - "node_modules/jsdom/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/jsdom/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "node": ">= 14" } }, "node_modules/jsdom/node_modules/saxes": { @@ -20835,9 +20827,9 @@ } }, "node_modules/jsdom/node_modules/tough-cookie": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", - "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -20849,14 +20841,14 @@ } }, "node_modules/jsdom/node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", "dependencies": { - "punycode": "^2.3.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/jsdom/node_modules/universalify": { @@ -20867,44 +20859,30 @@ "node": ">= 4.0.0" } }, - "node_modules/jsdom/node_modules/w3c-xmlserializer": { + "node_modules/jsdom/node_modules/whatwg-mimetype": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "dependencies": { - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/jsdom/node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dependencies": { - "iconv-lite": "0.6.3" - }, + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/jsdom/node_modules/whatwg-url": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", - "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", "dependencies": { - "tr46": "^4.1.1", + "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/jsdom/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", "engines": { "node": ">=10.0.0" }, @@ -20921,14 +20899,6 @@ } } }, - "node_modules/jsdom/node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "engines": { - "node": ">=12" - } - }, "node_modules/jsesc": { "version": "2.5.2", "license": "MIT", @@ -23497,9 +23467,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", - "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==" + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", + "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==" }, "node_modules/oauth-sign": { "version": "0.9.0", @@ -25006,9 +24976,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "engines": { "node": ">=6" } @@ -26080,9 +26050,9 @@ } }, "node_modules/rrweb-cssom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==" + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.0.tgz", + "integrity": "sha512-KlSv0pm9kgQSRxXEMgtivPJ4h826YHsuob8pSHcfSZsSXGtvpEAie8S0AnXuObEJ7nhikOb4ahwxDm0H2yW17g==" }, "node_modules/run-parallel": { "version": "1.1.9", @@ -29267,6 +29237,17 @@ "browser-process-hrtime": "^1.0.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -29888,6 +29869,28 @@ "node": ">=0.10.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/whatwg-fetch": { "version": "3.6.2", "license": "MIT" @@ -29896,6 +29899,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, "engines": { "node": ">=12" } @@ -30281,6 +30285,14 @@ "dev": true, "license": "MIT" }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "engines": { + "node": ">=18" + } + }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", diff --git a/package.json b/package.json index 262a9361f9..ba43a54fa9 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "intl-tel-input": "~12.4.0", "ip": "^1.1.9", "jose": "^4.15.5", - "jsdom": "^21.1.1", + "jsdom": "^24.1.0", "json-stringify-safe": "^5.0.1", "JSONStream": "^1.3.5", "jsonwebtoken": "^9.0.2", From 92ecd9affba7b55d1479518d2e3c902e961e5078 Mon Sep 17 00:00:00 2001 From: Ken Lee Shu Ming Date: Fri, 5 Jul 2024 13:35:20 +0800 Subject: [PATCH 10/16] feat(admin-dashboard): add response mode text next to form title on dashboard (#7462) * feat: add response mode text next to form title on dashboard * fix: address undeclared grid item on small screen --- .../features/admin-form/common/constants.ts | 7 +++++++ .../settings/components/GeneralTabHeader.tsx | 20 ++++++------------- .../WorkspaceFormRow/WorkspaceFormRow.tsx | 14 ++++++++++--- 3 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 frontend/src/features/admin-form/common/constants.ts diff --git a/frontend/src/features/admin-form/common/constants.ts b/frontend/src/features/admin-form/common/constants.ts new file mode 100644 index 0000000000..50b574a1fe --- /dev/null +++ b/frontend/src/features/admin-form/common/constants.ts @@ -0,0 +1,7 @@ +import { FormResponseMode } from '~shared/types' + +export const RESPONSE_MODE_TO_TEXT: { [key in FormResponseMode]: string } = { + [FormResponseMode.Multirespondent]: 'Multi-respondent form', + [FormResponseMode.Email]: 'Email mode', + [FormResponseMode.Encrypt]: 'Storage mode', +} diff --git a/frontend/src/features/admin-form/settings/components/GeneralTabHeader.tsx b/frontend/src/features/admin-form/settings/components/GeneralTabHeader.tsx index a8823f8206..08bba58986 100644 --- a/frontend/src/features/admin-form/settings/components/GeneralTabHeader.tsx +++ b/frontend/src/features/admin-form/settings/components/GeneralTabHeader.tsx @@ -1,10 +1,9 @@ -import { useMemo } from 'react' import { Skeleton, Wrap } from '@chakra-ui/react' -import { FormResponseMode } from '~shared/types/form' - import Badge from '~components/Badge' +import { RESPONSE_MODE_TO_TEXT } from '~features/admin-form/common/constants' + import { useAdminFormSettings } from '../queries' import { CategoryHeader } from './CategoryHeader' @@ -13,17 +12,10 @@ export const GeneralTabHeader = (): JSX.Element => { const { data: settings, isLoading: isLoadingSettings } = useAdminFormSettings() - const readableFormResponseMode = useMemo(() => { - switch (settings?.responseMode) { - case FormResponseMode.Email: - return 'Email mode' - case FormResponseMode.Encrypt: - return 'Storage mode' - case FormResponseMode.Multirespondent: - return 'Multi-respondent form' - } - return 'Loading...' - }, [settings?.responseMode]) + const readableFormResponseMode = !settings + ? 'Loading...' + : RESPONSE_MODE_TO_TEXT[settings.responseMode] + return ( + + + {RESPONSE_MODE_TO_TEXT[formMeta.responseMode]} + + From 5e6d4b3243667cbc9c95cb3abbc54ca2737a0fc7 Mon Sep 17 00:00:00 2001 From: Ken Lee Shu Ming Date: Fri, 5 Jul 2024 19:26:05 +0800 Subject: [PATCH 11/16] fix(reddot): hide on mrf (#7481) fix: hide reddot on mrf --- .../components/AdminFormNavbar/AdminFormNavbar.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/features/admin-form/common/components/AdminFormNavbar/AdminFormNavbar.tsx b/frontend/src/features/admin-form/common/components/AdminFormNavbar/AdminFormNavbar.tsx index 6c3bbae6cd..0928649b39 100644 --- a/frontend/src/features/admin-form/common/components/AdminFormNavbar/AdminFormNavbar.tsx +++ b/frontend/src/features/admin-form/common/components/AdminFormNavbar/AdminFormNavbar.tsx @@ -28,7 +28,7 @@ import { import format from 'date-fns/format' import { SeenFlags } from '~shared/types' -import { AdminFormDto } from '~shared/types/form/form' +import { AdminFormDto, FormResponseMode } from '~shared/types/form/form' import { ACTIVE_ADMINFORM_BUILDER_ROUTE_REGEX, @@ -55,7 +55,7 @@ export interface AdminFormNavbarProps { * Minimum form info needed to render the navbar. * If not provided, the navbar will be in a loading state. */ - formInfo?: Pick + formInfo?: AdminFormDto viewOnly: boolean handleAddCollabButtonClick: () => void handleShareButtonClick: () => void @@ -79,9 +79,11 @@ export const AdminFormNavbar = ({ const { user, isLoading: isUserLoading } = useUser() const { updateLastSeenFlagMutation } = useUserMutations() const shouldShowSettingsReddot = useMemo(() => { - if (isUserLoading || !user) return false + const isMrf = formInfo?.responseMode === FormResponseMode.Multirespondent + + if (isUserLoading || !user || isMrf) return false return getShowFeatureFlagLastSeen(user, SeenFlags.SettingsNotification) - }, [isUserLoading, user]) + }, [isUserLoading, user, formInfo?.responseMode]) const tabResponsiveVariant = useBreakpointValue({ base: 'line-dark', From d06fb4251195fc68f74682eac7214668325024ca Mon Sep 17 00:00:00 2001 From: Ken Lee Shu Ming Date: Fri, 5 Jul 2024 19:28:41 +0800 Subject: [PATCH 12/16] chore(deps): remove sentry (#7480) * remove sentry * remove sentry configs * test: update test cases --- .ebextensions/01env-file-aws-ssm.config | 1 - .template-env | 3 - __tests__/setup/.test-env | 4 +- docker-compose.yml | 2 - docs/DEPLOYMENT_SETUP.md | 32 ++--- package-lock.json | 123 ------------------ package.json | 2 - shared/types/core.ts | 1 - src/app/config/features/sentry.config.ts | 28 ---- .../loaders/express/__tests__/helmet.spec.ts | 6 - src/app/loaders/express/constants.ts | 1 - src/app/loaders/express/helmet.ts | 10 -- src/app/loaders/express/index.ts | 2 - src/app/loaders/express/locals.ts | 4 - src/app/loaders/express/sentry.ts | 33 ----- src/app/modules/frontend/frontend.service.ts | 2 - 16 files changed, 17 insertions(+), 237 deletions(-) delete mode 100644 src/app/config/features/sentry.config.ts delete mode 100644 src/app/loaders/express/sentry.ts diff --git a/.ebextensions/01env-file-aws-ssm.config b/.ebextensions/01env-file-aws-ssm.config index e31140a634..c20e5ae8e6 100644 --- a/.ebextensions/01env-file-aws-ssm.config +++ b/.ebextensions/01env-file-aws-ssm.config @@ -41,7 +41,6 @@ files: aws ssm get-parameter --name "${ENV_TYPE}-turnstile" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env aws ssm get-parameter --name "${ENV_TYPE}-ga" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env aws ssm get-parameter --name "${ENV_TYPE}-intranet" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env - aws ssm get-parameter --name "${ENV_TYPE}-sentry" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env aws ssm get-parameter --name "${ENV_TYPE}-sms" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env aws ssm get-parameter --name "${ENV_TYPE}-ndi" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env aws ssm get-parameter --name "${ENV_TYPE}-verified-fields" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env diff --git a/.template-env b/.template-env index d6ed66262c..ae390bdd66 100644 --- a/.template-env +++ b/.template-env @@ -59,9 +59,6 @@ FORMSG_SDK_MODE= # GOOGLE_CAPTCHA= # GOOGLE_CAPTCHA_PUBLIC= -## Sentry -## If the below variable exists, the [sentry] feature will be enabled. -# SENTRY_CONFIG_URL= ## Keys for storage mode # If forking this repository, you will also need to fork @opengovsg/formsg-sdk diff --git a/__tests__/setup/.test-env b/__tests__/setup/.test-env index 33e79d99c0..d168379265 100644 --- a/__tests__/setup/.test-env +++ b/__tests__/setup/.test-env @@ -36,9 +36,7 @@ IS_CP_MAINTENANCE=Date/Time-CP GOOGLE_CAPTCHA=123456789 GOOGLE_CAPTCHA_PUBLIC=987654321 -GA_TRACKING_ID=UA-123456789-0 -SENTRY_CONFIG_URL=https://random@sentry.io/1234567 -CSP_REPORT_URI=https://random@sentry.io/1234567 +GA_TRACKING_ID=UA-123456789-0 TWILIO_ACCOUNT_SID=ACrandomTwilioSid TWILIO_API_KEY=SKrandomTwilioAPIKEY diff --git a/docker-compose.yml b/docker-compose.yml index 66f71b07cc..79a8a5d3aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,8 +41,6 @@ services: - SES_HOST=maildev - WEBHOOK_SQS_URL=http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/local-webhooks-sqs-main - INTRANET_IP_LIST_PATH - - SENTRY_CONFIG_URL=https://random@sentry.io/123456 - - CSP_REPORT_URI=https://random@sentry.io/123456 # This needs to be removed and replaced with a real tracking ID in a local .env file # in order to enable GA in a local environment # TODO: remove after React rollout #4786 diff --git a/docs/DEPLOYMENT_SETUP.md b/docs/DEPLOYMENT_SETUP.md index 1653ec35d9..2f192f64ce 100644 --- a/docs/DEPLOYMENT_SETUP.md +++ b/docs/DEPLOYMENT_SETUP.md @@ -79,20 +79,20 @@ If no `msgSrvcName` is found in the form document, SMSes associated with that fo ### Github Actions Secrets The following repository secrets are set in Github Actions: -| Secret | Description| -|:---------|------------| -|`AWS_ACCESS_KEY_ID`|AWS IAM access key ID used to deploy.| -|`AWS_SECRET_ACCESS_KEY`|AWS IAM access secret used to deploy.| -|`AWS_DEFAULT_REGION`|AWS region to use.| -|`ECR_REPO`|ECR Repository which stores the docker images.| -|`BUCKET_NAME`| S3 Bucket used to store zipped `Dockerrun.aws.json`.| +| Secret | Description | +| :---------------------- | ---------------------------------------------------- | +| `AWS_ACCESS_KEY_ID` | AWS IAM access key ID used to deploy. | +| `AWS_SECRET_ACCESS_KEY` | AWS IAM access secret used to deploy. | +| `AWS_DEFAULT_REGION` | AWS region to use. | +| `ECR_REPO` | ECR Repository which stores the docker images. | +| `BUCKET_NAME` | S3 Bucket used to store zipped `Dockerrun.aws.json`. | There are also environment secrets for each environment (`staging`, `staging-alt`, `release`, `uat`): -| Secret | Description| -|:---------|------------| -|`APP_NAME`|Application name for the environment.| -|`DEPLOY_ENV`|Deployment environment on elastic beanstalk.| -|`REACT_APP_FORMSG_SDK_MODE`|Determines the keys used in the formsg SDK. Set either `production` or `staging`.| +| Secret | Description | +| :-------------------------- | --------------------------------------------------------------------------------- | +| `APP_NAME` | Application name for the environment. | +| `DEPLOY_ENV` | Deployment environment on elastic beanstalk. | +| `REACT_APP_FORMSG_SDK_MODE` | Determines the keys used in the formsg SDK. Set either `production` or `staging`. | ## Environment Variables @@ -244,14 +244,14 @@ SITE_BANNER_CONTENT=hello:This is an invalid banner type, and the full text will #### Rate limits at specific endpoints The app applies per-minute, per-IP rate limits at specific API endpoints as a security measure. The limits can be specified with the following environment variables. -| Variable | Description | -| :-------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `SUBMISSIONS_RATE_LIMIT` | Per-minute, per-IP request limit for each submissions endpoint. The limit is applied separately for the email mode and encrypt mode endpoints. | +| Variable | Description | +| :------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| `SUBMISSIONS_RATE_LIMIT` | Per-minute, per-IP request limit for each submissions endpoint. The limit is applied separately for the email mode and encrypt mode endpoints. | | `SEND_AUTH_OTP_RATE_LIMIT` | Per-minute, per-IP request limit for the endpoint which requests for new login OTPs for the admin console or mobile / email field verifications. | ### Additional Features -The app contains a number of additional features like Captcha protection, Sentry reporting and Google Analytics. Each of these features requires specific environment variables which are detailed below. +The app contains a number of additional features like Captcha protection. Each of these features requires specific environment variables which are detailed below. #### Google Captcha diff --git a/package-lock.json b/package-lock.json index 5a97afc6a2..2b303e2d09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,8 +19,6 @@ "@opengovsg/sgid-client": "^2.0.0", "@react-email/components": "^0.0.15", "@react-email/render": "^0.0.12", - "@sentry/browser": "^7.51.2", - "@sentry/integrations": "^6.19.7", "@stablelib/base64": "^1.0.1", "abortcontroller-polyfill": "^1.7.5", "aws-info": "^1.2.0", @@ -7420,113 +7418,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/@sentry-internal/tracing": { - "version": "7.51.2", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.51.2.tgz", - "integrity": "sha512-OBNZn7C4CyocmlSMUPfkY9ORgab346vTHu5kX35PgW5XR51VD2nO5iJCFbyFcsmmRWyCJcZzwMNARouc2V4V8A==", - "dependencies": { - "@sentry/core": "7.51.2", - "@sentry/types": "7.51.2", - "@sentry/utils": "7.51.2", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/browser": { - "version": "7.51.2", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.51.2.tgz", - "integrity": "sha512-FQFEaTFbvYHPQE2emFjNoGSy+jXplwzoM/XEUBRjrGo62lf8BhMvWnPeG3H3UWPgrWA1mq0amvHRwXUkwofk0g==", - "dependencies": { - "@sentry-internal/tracing": "7.51.2", - "@sentry/core": "7.51.2", - "@sentry/replay": "7.51.2", - "@sentry/types": "7.51.2", - "@sentry/utils": "7.51.2", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/core": { - "version": "7.51.2", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.51.2.tgz", - "integrity": "sha512-p8ZiSBxpKe+rkXDMEcgmdoyIHM/1bhpINLZUFPiFH8vzomEr7sgnwRhyrU8y/ADnkPeNg/2YF3QpDpk0OgZJUA==", - "dependencies": { - "@sentry/types": "7.51.2", - "@sentry/utils": "7.51.2", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/integrations": { - "version": "6.19.7", - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", - "localforage": "^1.8.1", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sentry/integrations/node_modules/@sentry/types": { - "version": "6.19.7", - "license": "BSD-3-Clause", - "engines": { - "node": ">=6" - } - }, - "node_modules/@sentry/integrations/node_modules/@sentry/utils": { - "version": "6.19.7", - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/types": "6.19.7", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sentry/replay": { - "version": "7.51.2", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.51.2.tgz", - "integrity": "sha512-W8YnSxkK9LTUXDaYciM7Hn87u57AX9qvH8jGcxZZnvpKqHlDXOpSV8LRtBkARsTwgLgswROImSifY0ic0lyCWg==", - "dependencies": { - "@sentry/core": "7.51.2", - "@sentry/types": "7.51.2", - "@sentry/utils": "7.51.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@sentry/types": { - "version": "7.51.2", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.51.2.tgz", - "integrity": "sha512-/hLnZVrcK7G5BQoD/60u9Qak8c9AvwV8za8TtYPJDUeW59GrqnqOkFji7RVhI7oH1OX4iBxV+9pAKzfYE6A6SA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/utils": { - "version": "7.51.2", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.51.2.tgz", - "integrity": "sha512-EcjBU7qG4IG+DpIPvdgIBcdIofROMawKoRUNKraeKzH/waEYH9DzCaqp/mzc5/rPBhpDB4BShX9xDDSeH+8c0A==", - "dependencies": { - "@sentry/types": "7.51.2", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@sideway/address": { "version": "4.1.3", "license": "BSD-3-Clause", @@ -21236,13 +21127,6 @@ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.59.tgz", "integrity": "sha512-HeTsOrDF/hWhEiKqZVwg9Cqlep5x2T+IYDENvT2VRj3iX8JQ7Y+omENv+AIn0vC8m6GYhivfCed5Cgfw27r5SA==" }, - "node_modules/lie": { - "version": "3.1.1", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/lilconfig": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", @@ -21566,13 +21450,6 @@ "node": ">=4.0.0" } }, - "node_modules/localforage": { - "version": "1.10.0", - "license": "Apache-2.0", - "dependencies": { - "lie": "3.1.1" - } - }, "node_modules/locate-path": { "version": "5.0.0", "license": "MIT", diff --git a/package.json b/package.json index ba43a54fa9..c9ebe57dc9 100644 --- a/package.json +++ b/package.json @@ -65,8 +65,6 @@ "@opengovsg/sgid-client": "^2.0.0", "@react-email/components": "^0.0.15", "@react-email/render": "^0.0.12", - "@sentry/browser": "^7.51.2", - "@sentry/integrations": "^6.19.7", "@stablelib/base64": "^1.0.1", "abortcontroller-polyfill": "^1.7.5", "aws-info": "^1.2.0", diff --git a/shared/types/core.ts b/shared/types/core.ts index b3d880bfbc..819d5a624e 100644 --- a/shared/types/core.ts +++ b/shared/types/core.ts @@ -21,7 +21,6 @@ export type ClientEnvVars = { formsgSdkMode: 'staging' | 'production' | 'development' | 'test' captchaPublicKey: string // Recaptcha turnstileSiteKey: string // Turnstile - sentryConfigUrl: string // Sentry.IO isSPMaintenance: string // Singpass maintenance message isCPMaintenance: string // Corppass maintenance message myInfoBannerContent: string // MyInfo maintenance message diff --git a/src/app/config/features/sentry.config.ts b/src/app/config/features/sentry.config.ts deleted file mode 100644 index efa4f4cd75..0000000000 --- a/src/app/config/features/sentry.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import convict, { Schema } from 'convict' -import { url } from 'convict-format-with-validator' - -export interface ISentry { - sentryConfigUrl: string - cspReportUri: string -} - -convict.addFormat(url) - -const sentryFeature: Schema = { - sentryConfigUrl: { - doc: 'Sentry.io URL for configuring the Sentry SDK', - format: 'url', - default: null, - env: 'SENTRY_CONFIG_URL', - }, - cspReportUri: { - doc: 'Endpoint for content security policy reporting', - format: 'url', - default: null, - env: 'CSP_REPORT_URI', - }, -} - -export const sentryConfig = convict(sentryFeature) - .validate({ allowed: 'strict' }) - .getProperties() diff --git a/src/app/loaders/express/__tests__/helmet.spec.ts b/src/app/loaders/express/__tests__/helmet.spec.ts index 8574013fe8..1dca8c8eaf 100644 --- a/src/app/loaders/express/__tests__/helmet.spec.ts +++ b/src/app/loaders/express/__tests__/helmet.spec.ts @@ -2,7 +2,6 @@ import expressHandler from '__tests__/unit/backend/helpers/jest-express' import helmet from 'helmet' import config from 'src/app/config/config' -import { sentryConfig } from 'src/app/config/features/sentry.config' import { CSP_CORE_DIRECTIVES } from '../constants' import helmetMiddlewares from '../helmet' @@ -12,8 +11,6 @@ describe('helmetMiddlewares', () => { const mockHelmet = jest.mocked(helmet) jest.mock('src/app/config/config') const mockConfig = jest.mocked(config) - jest.mock('src/app/config/features/sentry.config') - const mockSentryConfig = jest.mocked(sentryConfig) const cspCoreDirectives = CSP_CORE_DIRECTIVES @@ -85,20 +82,17 @@ describe('helmetMiddlewares', () => { }) it('should call helmet.contentSecurityPolicy() with the correct directives if cspReportUri and !isDev', () => { - mockSentryConfig.cspReportUri = 'value' mockConfig.isDev = false helmetMiddlewares() expect(mockHelmet.contentSecurityPolicy).toHaveBeenCalledWith({ useDefaults: true, directives: { ...cspCoreDirectives, - reportUri: ['value'], }, }) }) it('should call helmet.contentSecurityPolicy() with the correct directives if !cspReportUri and isDev', () => { - mockSentryConfig.cspReportUri = '' mockConfig.isDev = true helmetMiddlewares() expect(mockHelmet.contentSecurityPolicy).toHaveBeenCalledWith({ diff --git a/src/app/loaders/express/constants.ts b/src/app/loaders/express/constants.ts index c0b965cb28..588f73b1a5 100644 --- a/src/app/loaders/express/constants.ts +++ b/src/app/loaders/express/constants.ts @@ -37,7 +37,6 @@ export const CSP_CORE_DIRECTIVES = { 'https://www.google-analytics.com/', 'https://ssl.google-analytics.com/', 'https://*.browser-intake-datadoghq.com', - 'https://sentry.io/api/', config.aws.attachmentBucketUrl, config.aws.imageBucketUrl, config.aws.logoBucketUrl, diff --git a/src/app/loaders/express/helmet.ts b/src/app/loaders/express/helmet.ts index d7b4d9d1c8..f9a6440b86 100644 --- a/src/app/loaders/express/helmet.ts +++ b/src/app/loaders/express/helmet.ts @@ -3,7 +3,6 @@ import helmet from 'helmet' import { ContentSecurityPolicyOptions } from 'helmet/dist/types/middlewares/content-security-policy' import config from '../../config/config' -import { sentryConfig } from '../../config/features/sentry.config' import { CSP_CORE_DIRECTIVES } from './constants' @@ -32,17 +31,8 @@ const helmetMiddlewares = () => { const cspCoreDirectives: ContentSecurityPolicyOptions['directives'] = CSP_CORE_DIRECTIVES - const reportUri = sentryConfig.cspReportUri - const cspOptionalDirectives: ContentSecurityPolicyOptions['directives'] = {} - // Add on reportUri CSP header if ReportUri exists - // It is necessary to have the if statement for optional directives because falsey values - // do not work - e.g. cspOptionalDirectives.reportUri = [false] will still set the reportUri header - // See https://github.com/helmetjs/csp/issues/36 and - // https://github.com/helmetjs/helmet/blob/cb170160e7c1ccac314cc19d3b979cfc771f1349/middlewares/content-security-policy/index.ts#L135 - if (reportUri) cspOptionalDirectives.reportUri = [reportUri] - // Remove upgradeInsecureRequest CSP header if config.isDev // See https://github.com/helmetjs/helmet for use of null to disable default if (config.isDev) cspOptionalDirectives.upgradeInsecureRequests = null diff --git a/src/app/loaders/express/index.ts b/src/app/loaders/express/index.ts index 290ef52fd1..63454adb5d 100644 --- a/src/app/loaders/express/index.ts +++ b/src/app/loaders/express/index.ts @@ -24,7 +24,6 @@ import helmetMiddlewares from './helmet' import appLocals from './locals' import loggingMiddleware from './logging' import parserMiddlewares from './parser' -import sentryMiddlewares from './sentry' import sessionMiddlewares from './session' const loadExpressApp = async (connection: Connection) => { @@ -136,7 +135,6 @@ const loadExpressApp = async (connection: Connection) => { app.use('/', FrontendRouter) - app.use(sentryMiddlewares()) app.use(errorHandlerMiddlewares()) const server = http.createServer(app) diff --git a/src/app/loaders/express/locals.ts b/src/app/loaders/express/locals.ts index 75cc06e2f2..ee577cb0ae 100644 --- a/src/app/loaders/express/locals.ts +++ b/src/app/loaders/express/locals.ts @@ -4,7 +4,6 @@ import config from '../../config/config' import { captchaConfig } from '../../config/features/captcha.config' import { googleAnalyticsConfig } from '../../config/features/google-analytics.config' import { paymentConfig } from '../../config/features/payment.config' -import { sentryConfig } from '../../config/features/sentry.config' import { spcpMyInfoConfig } from '../../config/features/spcp-myinfo.config' // Construct js with environment variables needed by frontend @@ -16,7 +15,6 @@ const frontendVars = { logoBucketUrl: config.aws.logoBucketUrl, // S3 bucket formsgSdkMode: config.formsgSdkMode, captchaPublicKey: captchaConfig.captchaPublicKey, // Recaptcha - sentryConfigUrl: sentryConfig.sentryConfigUrl, // Sentry.IO isSPMaintenance: spcpMyInfoConfig.isSPMaintenance, // Singpass maintenance message isCPMaintenance: spcpMyInfoConfig.isCPMaintenance, // Corppass maintenance message myInfoBannerContent: spcpMyInfoConfig.myInfoBannerContent, // MyInfo maintenance message @@ -47,8 +45,6 @@ const environment = ejs.render( var GATrackingID = "<%= GATrackingID%>" // Recaptcha var captchaPublicKey = "<%= captchaPublicKey %>" - // Sentry.IO - var sentryConfigUrl = "<%= sentryConfigUrl%>" // S3 bucket var logoBucketUrl = "<%= logoBucketUrl%>" // Node env diff --git a/src/app/loaders/express/sentry.ts b/src/app/loaders/express/sentry.ts deleted file mode 100644 index 923ef9d7e7..0000000000 --- a/src/app/loaders/express/sentry.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { RequestHandler } from 'express' - -import config from '../../config/config' - -const sentryMiddlewares = () => { - const sentryHeadersMiddleware: RequestHandler = (req, res, next) => { - // Website you wish to allow to connect - res.setHeader('Access-Control-Allow-Origin', config.app.appUrl) - - // Request methods you wish to allow - res.setHeader( - 'Access-Control-Allow-Methods', - 'GET, POST, OPTIONS, PUT, PATCH, DELETE', - ) - - // Request headers you wish to allow - res.setHeader( - 'Access-Control-Allow-Headers', - 'X-Requested-With,content-type', - ) - - // Set to true if you need the website to include cookies in the requests sent - // to the API (e.g. in case you use sessions) - res.setHeader('Access-Control-Allow-Credentials', 'true') - - // Pass to next layer of middleware - return next() - } - - return [sentryHeadersMiddleware] -} - -export default sentryMiddlewares diff --git a/src/app/modules/frontend/frontend.service.ts b/src/app/modules/frontend/frontend.service.ts index f97dfa8c29..1342913530 100644 --- a/src/app/modules/frontend/frontend.service.ts +++ b/src/app/modules/frontend/frontend.service.ts @@ -5,7 +5,6 @@ import { goGovConfig } from '../../config/features/gogov.config' import { googleAnalyticsConfig } from '../../config/features/google-analytics.config' import { growthbookConfig } from '../../config/features/growthbook.config' import { paymentConfig } from '../../config/features/payment.config' -import { sentryConfig } from '../../config/features/sentry.config' import { spcpMyInfoConfig } from '../../config/features/spcp-myinfo.config' import { turnstileConfig } from '../../config/features/turnstile.config' @@ -19,7 +18,6 @@ export const getClientEnvVars = (): ClientEnvVars => { formsgSdkMode: config.formsgSdkMode, captchaPublicKey: captchaConfig.captchaPublicKey, // Recaptcha turnstileSiteKey: turnstileConfig.turnstileSiteKey, - sentryConfigUrl: sentryConfig.sentryConfigUrl, // Sentry.IO isSPMaintenance: spcpMyInfoConfig.isSPMaintenance, // Singpass maintenance message isCPMaintenance: spcpMyInfoConfig.isCPMaintenance, // Corppass maintenance message myInfoBannerContent: spcpMyInfoConfig.myInfoBannerContent, // MyInfo maintenance message From fc527497ff6e1337fc60a98be261a80633033c6b Mon Sep 17 00:00:00 2001 From: Tejas G Date: Fri, 5 Jul 2024 19:56:31 +0800 Subject: [PATCH 13/16] feat: rename placeholder for logic and mrf components (#7482) --- .../EditLogicBlock/EditCondition/EditConditionBlock.tsx | 4 ++-- .../WorkflowContent/EditStepBlock/RespondentBlock.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/features/admin-form/create/logic/components/LogicContent/EditLogicBlock/EditCondition/EditConditionBlock.tsx b/frontend/src/features/admin-form/create/logic/components/LogicContent/EditLogicBlock/EditCondition/EditConditionBlock.tsx index 57dc561539..18f329322c 100644 --- a/frontend/src/features/admin-form/create/logic/components/LogicContent/EditLogicBlock/EditCondition/EditConditionBlock.tsx +++ b/frontend/src/features/admin-form/create/logic/components/LogicContent/EditLogicBlock/EditCondition/EditConditionBlock.tsx @@ -308,7 +308,7 @@ export const EditConditionBlock = ({ control={control} name={`${name}.field`} rules={{ - required: 'Please select a question.', + required: 'Please select a field.', validate: (value) => !logicableFields || Object.keys(logicableFields).includes(value) || @@ -318,7 +318,7 @@ export const EditConditionBlock = ({ diff --git a/frontend/src/features/admin-form/create/workflow/components/WorkflowContent/EditStepBlock/RespondentBlock.tsx b/frontend/src/features/admin-form/create/workflow/components/WorkflowContent/EditStepBlock/RespondentBlock.tsx index a9c5ee3eb7..59c540c537 100644 --- a/frontend/src/features/admin-form/create/workflow/components/WorkflowContent/EditStepBlock/RespondentBlock.tsx +++ b/frontend/src/features/admin-form/create/workflow/components/WorkflowContent/EditStepBlock/RespondentBlock.tsx @@ -170,7 +170,7 @@ const RespondentInput = ({ isLoading, formMethods }: RespondentInputProps) => { control={control} name="field" rules={{ - required: 'Please select a question', + required: 'Please select a field', validate: (value) => !emailFormFields || emailFormFields.some(({ _id }) => _id === value) || @@ -180,7 +180,7 @@ const RespondentInput = ({ isLoading, formMethods }: RespondentInputProps) => { Date: Fri, 5 Jul 2024 19:59:34 +0800 Subject: [PATCH 14/16] chore(deps): bump next and react-email in /react-email-preview (#7375) Bumps [next](https://github.com/vercel/next.js) to 14.1.4 and updates ancestor dependency [react-email](https://github.com/resend/react-email/tree/HEAD/packages/react-email). These dependencies need to be updated together. Updates `next` from 14.1.0 to 14.1.4 - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v14.1.0...v14.1.4) Updates `react-email` from 2.1.0 to 2.1.4 - [Release notes](https://github.com/resend/react-email/releases) - [Commits](https://github.com/resend/react-email/commits/react-email@2.1.4/packages/react-email) --- updated-dependencies: - dependency-name: next dependency-type: indirect - dependency-name: react-email dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- react-email-preview/package-lock.json | 1145 +++++++++++-------------- react-email-preview/package.json | 2 +- 2 files changed, 497 insertions(+), 650 deletions(-) diff --git a/react-email-preview/package-lock.json b/react-email-preview/package-lock.json index 5b4ef2a2db..e48286e9d5 100644 --- a/react-email-preview/package-lock.json +++ b/react-email-preview/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@react-email/components": "0.0.15", "react": "18.2.0", - "react-email": "2.1.0" + "react-email": "2.1.4" }, "devDependencies": { "@types/react": "18.2.33", @@ -28,32 +28,254 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dependencies": { - "@babel/highlight": "^7.24.2", + "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", - "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -99,14 +321,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@babel/highlight/node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -126,6 +340,17 @@ "node": ">=4" } }, + "node_modules/@babel/parser": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", @@ -137,6 +362,74 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -498,28 +791,28 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", - "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.3.tgz", + "integrity": "sha512-1ZpCvYf788/ZXOhRQGFxnYQOVgeU+pi0i+d0Ow34La7qjIXETi6RNswGVKkA6KcDO8/+Ysu2E/CeUmmeEBDvTg==", "dependencies": { - "@floating-ui/utils": "^0.2.1" + "@floating-ui/utils": "^0.2.3" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", - "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.6.tgz", + "integrity": "sha512-qiTYajAnh3P+38kECeffMSQgbvXty2VB6rS+42iWR4FPIlZjLK84E9qtLnMTLIpPz2znD/TaFqaiavMUrS+Hcw==", "dependencies": { "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" + "@floating-ui/utils": "^0.2.3" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", - "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", + "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", "dependencies": { - "@floating-ui/dom": "^1.6.1" + "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -527,9 +820,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.3.tgz", + "integrity": "sha512-XGndio0l5/Gvd6CLIABvsav9HHezgDFFhDfHk1bvLfr9ni8dojqLSvBbotJEjmIwNHL7vK4QzBJTdBRoB+c1ww==" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -600,14 +893,14 @@ } }, "node_modules/@next/env": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz", - "integrity": "sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==" + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.4.tgz", + "integrity": "sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==" }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz", - "integrity": "sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==", + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.4.tgz", + "integrity": "sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg==", "cpu": [ "arm64" ], @@ -620,9 +913,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz", - "integrity": "sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==", + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.4.tgz", + "integrity": "sha512-b0Xo1ELj3u7IkZWAKcJPJEhBop117U78l70nfoQGo4xUSvv0PJSTaV4U9xQBLvZlnjsYkc8RwQN1HoH/oQmLlQ==", "cpu": [ "x64" ], @@ -635,9 +928,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz", - "integrity": "sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==", + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.4.tgz", + "integrity": "sha512-457G0hcLrdYA/u1O2XkRMsDKId5VKe3uKPvrKVOyuARa6nXrdhJOOYU9hkKKyQTMru1B8qEP78IAhf/1XnVqKA==", "cpu": [ "arm64" ], @@ -650,9 +943,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz", - "integrity": "sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==", + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.4.tgz", + "integrity": "sha512-l/kMG+z6MB+fKA9KdtyprkTQ1ihlJcBh66cf0HvqGP+rXBbOXX0dpJatjZbHeunvEHoBBS69GYQG5ry78JMy3g==", "cpu": [ "arm64" ], @@ -665,9 +958,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz", - "integrity": "sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==", + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.4.tgz", + "integrity": "sha512-BapIFZ3ZRnvQ1uWbmqEGJuPT9cgLwvKtxhK/L2t4QYO7l+/DxXuIGjvp1x8rvfa/x1FFSsipERZK70pewbtJtw==", "cpu": [ "x64" ], @@ -680,9 +973,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz", - "integrity": "sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==", + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.4.tgz", + "integrity": "sha512-mqVxTwk4XuBl49qn2A5UmzFImoL1iLm0KQQwtdRJRKl21ylQwwGCxJtIYo2rbfkZHoSKlh/YgztY0qH3wG1xIg==", "cpu": [ "x64" ], @@ -695,9 +988,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz", - "integrity": "sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==", + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.4.tgz", + "integrity": "sha512-xzxF4ErcumXjO2Pvg/wVGrtr9QQJLk3IyQX1ddAC/fi6/5jZCZ9xpuL9Tzc4KPWMFq8GGWFVDMshZOdHGdkvag==", "cpu": [ "arm64" ], @@ -710,9 +1003,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz", - "integrity": "sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==", + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.4.tgz", + "integrity": "sha512-WZiz8OdbkpRw6/IU/lredZWKKZopUMhcI2F+XiMAcPja0uZYdMTZQRoQ0WZcvinn9xZAidimE7tN9W5v9Yyfyw==", "cpu": [ "ia32" ], @@ -725,9 +1018,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz", - "integrity": "sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==", + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.4.tgz", + "integrity": "sha512-4Rto21sPfw555sZ/XNLqfxDUNeLhNYGO2dlPqsnuCg8N8a2a9u1ltqBOPQ4vj1Gf7eJC0W2hHG2eYUHuiXgY2w==", "cpu": [ "x64" ], @@ -929,9 +1222,9 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz", - "integrity": "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", @@ -973,9 +1266,9 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz", - "integrity": "sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", @@ -1016,20 +1309,20 @@ } }, "node_modules/@radix-ui/react-popover": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.6.tgz", - "integrity": "sha512-cZ4defGpkZ0qTRtlIBzJLSzL6ht7ofhhW4i1+pkemjV1IKXm0wgCRnee154qlV6r9Ttunmh2TNZhMfV2bavUyA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz", + "integrity": "sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==", "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.4", + "@radix-ui/react-dismissable-layer": "1.0.5", "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.3", + "@radix-ui/react-focus-scope": "1.0.4", "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.2", - "@radix-ui/react-portal": "1.0.3", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", "@radix-ui/react-presence": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-slot": "1.0.2", @@ -1053,9 +1346,9 @@ } }, "node_modules/@radix-ui/react-popper": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz", - "integrity": "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", + "integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==", "dependencies": { "@babel/runtime": "^7.13.10", "@floating-ui/react-dom": "^2.0.0", @@ -1085,9 +1378,9 @@ } }, "node_modules/@radix-ui/react-portal": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.3.tgz", - "integrity": "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.3" @@ -1258,18 +1551,18 @@ } }, "node_modules/@radix-ui/react-tooltip": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.6.tgz", - "integrity": "sha512-DmNFOiwEc2UDigsYj6clJENma58OelxD24O4IODoZ+3sQc3Zb+L8w1EP+y9laTuKCLAysPw4fD6/v0j4KNV8rg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", + "integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==", "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.4", + "@radix-ui/react-dismissable-layer": "1.0.5", "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.2", - "@radix-ui/react-portal": "1.0.3", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", "@radix-ui/react-presence": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-slot": "1.0.2", @@ -1956,11 +2249,6 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==" - }, "node_modules/@types/node": { "version": "20.11.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", @@ -1969,11 +2257,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==" - }, "node_modules/@types/prismjs": { "version": "1.26.3", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.3.tgz", @@ -2278,14 +2561,6 @@ "node": ">=10" } }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/autoprefixer": { "version": "10.4.14", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", @@ -2371,11 +2646,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -2465,14 +2735,6 @@ "node": ">=10.16.0" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -2481,22 +2743,6 @@ "node": ">= 6" } }, - "node_modules/camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dependencies": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001600", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz", @@ -2576,14 +2822,6 @@ "node": ">=8" } }, - "node_modules/cli-spinner": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/cli-spinner/-/cli-spinner-0.2.10.tgz", - "integrity": "sha512-U0sSQ+JJvSLi1pAYuJykwiA8Dsr15uHEy85iCJ6A+0DjVxivr3d+N2Wjvodeg89uP5K6TswFkKBfAD7B3YSn/Q==", - "engines": { - "node": ">=0.10" - } - }, "node_modules/cli-spinners": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", @@ -2654,6 +2892,11 @@ "proto-list": "~1.2.1" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, "node_modules/cookie": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", @@ -2730,37 +2973,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -2949,14 +3161,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-module-lexer": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", @@ -3007,6 +3211,14 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/eslint-config-prettier": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", @@ -3187,6 +3399,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -3232,38 +3452,19 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "node_modules/hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3283,11 +3484,6 @@ "node": ">= 0.4" } }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" - }, "node_modules/html-to-text": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", @@ -3340,14 +3536,6 @@ } ] }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "engines": { - "node": ">=8" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3383,11 +3571,6 @@ "loose-envify": "^1.0.0" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3453,14 +3636,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -3562,6 +3737,17 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -3572,12 +3758,15 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, "node_modules/leac": { @@ -3609,11 +3798,6 @@ "node": ">=6.11.5" } }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -3648,17 +3832,6 @@ "node": "14 || >=16.14" } }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/marked": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.4.tgz", @@ -3682,41 +3855,6 @@ "react-email": ">1.9.3" } }, - "node_modules/meow": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-7.1.1.tgz", - "integrity": "sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==", - "dependencies": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^2.5.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.13.1", - "yargs-parser": "^18.1.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3769,14 +3907,6 @@ "node": ">=6" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "engines": { - "node": ">=4" - } - }, "node_modules/minimatch": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", @@ -3791,19 +3921,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/minipass": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", @@ -3858,11 +3975,11 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "node_modules/next": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/next/-/next-14.1.0.tgz", - "integrity": "sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==", + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/next/-/next-14.1.4.tgz", + "integrity": "sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ==", "dependencies": { - "@next/env": "14.1.0", + "@next/env": "14.1.4", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -3877,15 +3994,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.1.0", - "@next/swc-darwin-x64": "14.1.0", - "@next/swc-linux-arm64-gnu": "14.1.0", - "@next/swc-linux-arm64-musl": "14.1.0", - "@next/swc-linux-x64-gnu": "14.1.0", - "@next/swc-linux-x64-musl": "14.1.0", - "@next/swc-win32-arm64-msvc": "14.1.0", - "@next/swc-win32-ia32-msvc": "14.1.0", - "@next/swc-win32-x64-msvc": "14.1.0" + "@next/swc-darwin-arm64": "14.1.4", + "@next/swc-darwin-x64": "14.1.4", + "@next/swc-linux-arm64-gnu": "14.1.4", + "@next/swc-linux-arm64-musl": "14.1.4", + "@next/swc-linux-x64-gnu": "14.1.4", + "@next/swc-linux-x64-musl": "14.1.4", + "@next/swc-win32-arm64-msvc": "14.1.4", + "@next/swc-win32-ia32-msvc": "14.1.4", + "@next/swc-win32-x64-msvc": "14.1.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -3948,25 +4065,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4062,31 +4160,6 @@ "node": ">=8" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parseley": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", @@ -4099,14 +4172,6 @@ "url": "https://ko-fi.com/killymxi" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -4384,14 +4449,6 @@ } ] }, - "node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "engines": { - "node": ">=8" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -4424,18 +4481,18 @@ } }, "node_modules/react-email": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-2.1.0.tgz", - "integrity": "sha512-fTt85ca1phsBu57iy32wn4LTR37rOzDZoY2AOWVq3JQYVwk6GlBdUuQWif2cudkwWINL9COf9kRMS4/QWtKtAQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-2.1.4.tgz", + "integrity": "sha512-YKZ4jhkalWcNyaw4qyI//+QeTeUxe/ptqI+wSc4wVIoHzqffAWoV5x/jBpFex3FQ636xVIDFrvGq39rUVL7zSQ==", "dependencies": { + "@babel/core": "7.24.5", + "@babel/parser": "7.24.5", "@radix-ui/colors": "1.0.1", "@radix-ui/react-collapsible": "1.0.3", - "@radix-ui/react-popover": "1.0.6", + "@radix-ui/react-popover": "1.0.7", "@radix-ui/react-slot": "1.0.2", "@radix-ui/react-toggle-group": "1.0.4", - "@radix-ui/react-tooltip": "1.0.6", - "@react-email/components": "0.0.15", - "@react-email/render": "0.0.12", + "@radix-ui/react-tooltip": "1.0.7", "@swc/core": "1.3.101", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", @@ -4453,10 +4510,10 @@ "glob": "10.3.4", "log-symbols": "4.1.0", "mime-types": "2.1.35", - "next": "14.1.0", + "next": "14.1.4", "normalize-path": "3.0.0", "ora": "5.4.1", - "postcss": "8.4.35", + "postcss": "8.4.38", "prism-react-renderer": "2.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -4468,7 +4525,6 @@ "stacktrace-parser": "0.1.10", "tailwind-merge": "2.2.0", "tailwindcss": "3.4.0", - "tree-cli": "0.6.7", "typescript": "5.1.6" }, "bin": { @@ -4507,6 +4563,41 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/react-email/node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react-email/node_modules/postcss/node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-remove-scroll": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", @@ -4582,100 +4673,6 @@ "pify": "^2.3.0" } }, - "node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "engines": { - "node": ">=8" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -4711,18 +4708,6 @@ "node": ">= 0.10" } }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -5051,34 +5036,6 @@ "source-map": "^0.6.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==" - }, "node_modules/stacktrace-parser": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", @@ -5202,17 +5159,6 @@ "node": ">=8" } }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -5427,6 +5373,14 @@ "node": ">=0.8" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5438,92 +5392,6 @@ "node": ">=8.0" } }, - "node_modules/tree-cli": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/tree-cli/-/tree-cli-0.6.7.tgz", - "integrity": "sha512-jfnB5YKY6Glf6bsFmQ9W97TtkPVLnHsjOR6ZdRf4zhyFRQeLheasvzE5XBJI2Hxt7ZyMyIbXUV7E2YPZbixgtA==", - "dependencies": { - "bluebird": "^3.4.6", - "chalk": "^1.1.3", - "cli-spinner": "^0.2.5", - "lodash.includes": "^4.3.0", - "meow": "^7.1.1", - "object-assign": "^4.1.0" - }, - "bin": { - "tree": "bin/tree", - "treee": "bin/tree" - }, - "engines": { - "node": ">=8.10.9" - } - }, - "node_modules/tree-cli/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tree-cli/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tree-cli/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tree-cli/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/tree-cli/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tree-cli/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "engines": { - "node": ">=8" - } - }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -5634,15 +5502,6 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -5888,18 +5747,6 @@ "engines": { "node": ">= 14" } - }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } } } } diff --git a/react-email-preview/package.json b/react-email-preview/package.json index 1feb6d767f..6b4ab54177 100644 --- a/react-email-preview/package.json +++ b/react-email-preview/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@react-email/components": "0.0.15", - "react-email": "2.1.0", + "react-email": "2.1.4", "react": "18.2.0" }, "devDependencies": { From f8763c58f769f4400dcc9b9d0b760c45b6f64624 Mon Sep 17 00:00:00 2001 From: Ken Lee Shu Ming Date: Sat, 6 Jul 2024 01:42:41 +0800 Subject: [PATCH 15/16] feat(btn): frm 1723 internal flow postman (#7343) * add postman sms service * feat: add user betaflag for postman sms * add sendBouncedSubmissionSms * test: add sendBouncedSubmissionSms cases * add sendFormDeactivatedSms * add sendAdminContactOtp * add package-lock * remove non-mop sms services, point sms.errors to postman-sms.errors * migrate non-mop smses to use postman sms service * update tests to remove migrated functions * fix incorrect api key reference * fix form.admin not populated before passing into flag field * update test cases for bounce, sms service * update test cases for bounce service * update test cases for user controller * update sms error import path * refactor: rename mop sms to use internal api convention, fix typo on logmeta.action * chore: fix build failures due to merge conflicts * chore: remove redundant logs --- .template-env | 8 +- __tests__/setup/.test-env | 2 + docker-compose.yml | 2 + src/app/config/features/postman-sms.config.ts | 20 +- .../bounce/__tests__/bounce.service.spec.ts | 187 ++++---- src/app/modules/bounce/bounce.errors.ts | 5 +- src/app/modules/bounce/bounce.service.ts | 34 +- .../user/__tests__/user.controller.spec.ts | 16 +- src/app/modules/user/user.controller.ts | 4 +- src/app/modules/user/user.utils.ts | 2 +- .../__tests__/verification.controller.spec.ts | 2 +- .../__tests__/verification.service.spec.ts | 2 +- .../verification/verification.service.ts | 5 +- .../modules/verification/verification.util.ts | 5 +- .../public-forms.verification.routes.spec.ts | 2 +- .../api/v3/user/__tests__/user.routes.spec.ts | 10 +- .../__tests__/postman-sms.service.spec.ts | 436 +++++++----------- .../postman-sms/postman-sms.service.ts | 207 ++++++++- .../services/postman-sms/postman-sms.types.ts | 15 + .../sms/__tests__/sms.factory.spec.ts | 56 +-- .../sms/__tests__/sms.service.spec.ts | 249 +--------- src/app/services/sms/sms.errors.ts | 16 - src/app/services/sms/sms.factory.ts | 49 +- src/app/services/sms/sms.service.ts | 147 +----- src/app/services/sms/sms.util.ts | 20 - 25 files changed, 579 insertions(+), 922 deletions(-) delete mode 100644 src/app/services/sms/sms.errors.ts delete mode 100644 src/app/services/sms/sms.util.ts diff --git a/.template-env b/.template-env index ae390bdd66..2007b47f3e 100644 --- a/.template-env +++ b/.template-env @@ -114,4 +114,10 @@ FORMSG_SDK_MODE= # Used to check if BE Server is currently running on local development environment # One of boolean: "true" | "false" # USE_MOCK_TWILIO= -# USE_MOCK_POSTMAN_SMS= \ No newline at end of file +# USE_MOCK_POSTMAN_SMS= + +# POSTMAN_MOP_CAMPAIGN_ID= +# POSTMAN_MOP_CAMPAIGN_API_KEY= +# POSTMAN_INTERNAL_CAMPAIGN_ID= +# POSTMAN_INTERNAL_CAMPAIGN_API_KEY= +# POSTMAN_BASE_URL=https://test.postman.gov.sg/api/v2 \ No newline at end of file diff --git a/__tests__/setup/.test-env b/__tests__/setup/.test-env index d168379265..4120ccb9cb 100644 --- a/__tests__/setup/.test-env +++ b/__tests__/setup/.test-env @@ -91,4 +91,6 @@ SSM_ENV_SITE_NAME=test API_KEY_VERSION=v1 POSTMAN_MOP_CAMPAIGN_ID=campaign_tesst POSTMAN_MOP_CAMPAIGN_API_KEY=key_test_123 +POSTMAN_INTERNAL_CAMPAIGN_ID=campaign_tesst +POSTMAN_INTERNAL_CAMPAIGN_API_KEY=key_test_123 POSTMAN_BASE_URL=https://test.postman.gov.sg/api/v2 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 79a8a5d3aa..9cee64c88b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -133,6 +133,8 @@ services: - WOGAA_FEEDBACK_ENDPOINT - POSTMAN_MOP_CAMPAIGN_ID=campaign_test - POSTMAN_MOP_CAMPAIGN_API_KEY=key_test_123 + - POSTMAN_INTERNAL_CAMPAIGN_ID=campaign_test_2 + - POSTMAN_INTERNAL_CAMPAIGN_API_KEY=key_test_456 - POSTMAN_BASE_URL=https://test.postman.gov.sg/api/v2 mockpass: diff --git a/src/app/config/features/postman-sms.config.ts b/src/app/config/features/postman-sms.config.ts index 52370451e7..c774a7cef2 100644 --- a/src/app/config/features/postman-sms.config.ts +++ b/src/app/config/features/postman-sms.config.ts @@ -1,12 +1,16 @@ import convict, { Schema } from 'convict' -export interface ISms { +export interface IPostmanSms { + // MOP SMSes are to be sent using GovSG sender id mopCampaignId: string mopCampaignApiKey: string + // Internal SMSes are to be sent using FormSG sender id + internalCampaignId: string + internalCampaignApiKey: string postmanBaseUrl: string } -const postmanSmsSchema: Schema = { +const postmanSmsSchema: Schema = { mopCampaignId: { doc: 'Postman SMS messaging campaign ID', format: String, @@ -19,6 +23,18 @@ const postmanSmsSchema: Schema = { default: null, env: 'POSTMAN_MOP_CAMPAIGN_API_KEY', }, + internalCampaignId: { + doc: 'Postman SMS messaging internal (non-mop) campaign ID', + format: String, + default: null, + env: 'POSTMAN_INTERNAL_CAMPAIGN_ID', + }, + internalCampaignApiKey: { + doc: 'Postman SMS messaging internal (non-mop) campaign ID', + format: String, + default: null, + env: 'POSTMAN_INTERNAL_CAMPAIGN_API_KEY', + }, postmanBaseUrl: { doc: 'Postman base URL', format: String, diff --git a/src/app/modules/bounce/__tests__/bounce.service.spec.ts b/src/app/modules/bounce/__tests__/bounce.service.spec.ts index 00eda8453b..7b9052ecb5 100644 --- a/src/app/modules/bounce/__tests__/bounce.service.spec.ts +++ b/src/app/modules/bounce/__tests__/bounce.service.spec.ts @@ -12,7 +12,7 @@ import getFormModel from 'src/app/models/form.server.model' import * as UserService from 'src/app/modules/user/user.service' import { EMAIL_HEADERS, EmailType } from 'src/app/services/mail/mail.constants' import MailService from 'src/app/services/mail/mail.service' -import { SmsFactory } from 'src/app/services/sms/sms.factory' +import PostmanSmsService from 'src/app/services/postman-sms/postman-sms.service' import { BounceType, IPopulatedForm, @@ -32,13 +32,12 @@ jest.mock('src/app/config/logger') const MockLoggerModule = jest.mocked(LoggerModule) jest.mock('src/app/services/mail/mail.service') const MockMailService = jest.mocked(MailService) -jest.mock('src/app/services/sms/sms.factory', () => ({ - SmsFactory: { - sendFormDeactivatedSms: jest.fn(), - sendBouncedSubmissionSms: jest.fn(), - }, +jest.mock('src/app/services/postman-sms/postman-sms.service', () => ({ + sendFormDeactivatedSms: jest.fn(), + sendBouncedSubmissionSms: jest.fn(), })) -const MockSmsFactory = jest.mocked(SmsFactory) +const MockedPostmanSmsService = jest.mocked(PostmanSmsService) + jest.mock('src/app/modules/user/user.service') const MockUserService = jest.mocked(UserService) @@ -53,7 +52,7 @@ import * as BounceService from 'src/app/modules/bounce/bounce.service' import { InvalidNumberError, SmsSendError, -} from 'src/app/services/sms/sms.errors' +} from 'src/app/services/postman-sms/postman-sms.errors' import { InvalidNotificationError, @@ -399,7 +398,9 @@ describe('BounceService', () => { formId: form._id, bounces: [], }) - MockSmsFactory.sendBouncedSubmissionSms.mockReturnValue(okAsync(true)) + MockedPostmanSmsService.sendBouncedSubmissionSms.mockReturnValue( + okAsync(true), + ) const notifiedRecipients = await BounceService.sendSmsBounceNotification( bounceDoc, @@ -407,23 +408,29 @@ describe('BounceService', () => { [MOCK_CONTACT, MOCK_CONTACT_2], ) - expect(MockSmsFactory.sendBouncedSubmissionSms).toHaveBeenCalledTimes(2) - expect(MockSmsFactory.sendBouncedSubmissionSms).toHaveBeenCalledWith({ - adminEmail: testUser.email, - adminId: String(testUser._id), - formId: form._id, - formTitle: form.title, - recipient: MOCK_CONTACT.contact, - recipientEmail: MOCK_CONTACT.email, - }) - expect(MockSmsFactory.sendBouncedSubmissionSms).toHaveBeenCalledWith({ - adminEmail: testUser.email, - adminId: String(testUser._id), - formId: form._id, - formTitle: form.title, - recipient: MOCK_CONTACT_2.contact, - recipientEmail: MOCK_CONTACT_2.email, - }) + expect( + MockedPostmanSmsService.sendBouncedSubmissionSms, + ).toHaveBeenCalledTimes(2) + expect( + MockedPostmanSmsService.sendBouncedSubmissionSms, + ).toHaveBeenCalledWith( + testUser.email, + String(testUser._id), + form._id, + form.title, + MOCK_CONTACT.contact, + MOCK_CONTACT.email, + ) + expect( + MockedPostmanSmsService.sendBouncedSubmissionSms, + ).toHaveBeenCalledWith( + testUser.email, + String(testUser._id), + form._id, + form.title, + MOCK_CONTACT_2.contact, + MOCK_CONTACT_2.email, + ) expect(notifiedRecipients._unsafeUnwrap()).toEqual([ MOCK_CONTACT, MOCK_CONTACT_2, @@ -439,7 +446,7 @@ describe('BounceService', () => { formId: form._id, bounces: [], }) - MockSmsFactory.sendBouncedSubmissionSms + MockedPostmanSmsService.sendBouncedSubmissionSms .mockReturnValueOnce(okAsync(true)) .mockReturnValueOnce(errAsync(new InvalidNumberError())) @@ -449,23 +456,29 @@ describe('BounceService', () => { [MOCK_CONTACT, MOCK_CONTACT_2], ) - expect(MockSmsFactory.sendBouncedSubmissionSms).toHaveBeenCalledTimes(2) - expect(MockSmsFactory.sendBouncedSubmissionSms).toHaveBeenCalledWith({ - adminEmail: testUser.email, - adminId: String(testUser._id), - formId: form._id, - formTitle: form.title, - recipient: MOCK_CONTACT.contact, - recipientEmail: MOCK_CONTACT.email, - }) - expect(MockSmsFactory.sendBouncedSubmissionSms).toHaveBeenCalledWith({ - adminEmail: testUser.email, - adminId: String(testUser._id), - formId: form._id, - formTitle: form.title, - recipient: MOCK_CONTACT_2.contact, - recipientEmail: MOCK_CONTACT_2.email, - }) + expect( + MockedPostmanSmsService.sendBouncedSubmissionSms, + ).toHaveBeenCalledTimes(2) + expect( + MockedPostmanSmsService.sendBouncedSubmissionSms, + ).toHaveBeenCalledWith( + testUser.email, + String(testUser._id), + form._id, + form.title, + MOCK_CONTACT.contact, + MOCK_CONTACT.email, + ) + expect( + MockedPostmanSmsService.sendBouncedSubmissionSms, + ).toHaveBeenCalledWith( + testUser.email, + String(testUser._id), + form._id, + form.title, + MOCK_CONTACT_2.contact, + MOCK_CONTACT_2.email, + ) expect(notifiedRecipients._unsafeUnwrap()).toEqual([MOCK_CONTACT]) }) }) @@ -838,7 +851,9 @@ describe('BounceService', () => { admin: testUser._id, title: MOCK_FORM_TITLE, }).populate('admin')) as IPopulatedForm - MockSmsFactory.sendFormDeactivatedSms.mockReturnValue(okAsync(true)) + MockedPostmanSmsService.sendFormDeactivatedSms.mockReturnValue( + okAsync(true), + ) const result = await BounceService.notifyAdminsOfDeactivation(form, [ MOCK_CONTACT, @@ -846,23 +861,29 @@ describe('BounceService', () => { ]) expect(result._unsafeUnwrap()).toEqual(true) - expect(MockSmsFactory.sendFormDeactivatedSms).toHaveBeenCalledTimes(2) - expect(MockSmsFactory.sendFormDeactivatedSms).toHaveBeenCalledWith({ - adminEmail: form.admin.email, - adminId: String(form.admin._id), - formId: form._id, - formTitle: form.title, - recipient: MOCK_CONTACT.contact, - recipientEmail: MOCK_CONTACT.email, - }) - expect(MockSmsFactory.sendFormDeactivatedSms).toHaveBeenCalledWith({ - adminEmail: form.admin.email, - adminId: String(form.admin._id), - formId: form._id, - formTitle: form.title, - recipient: MOCK_CONTACT_2.contact, - recipientEmail: MOCK_CONTACT_2.email, - }) + expect( + MockedPostmanSmsService.sendFormDeactivatedSms, + ).toHaveBeenCalledTimes(2) + expect( + MockedPostmanSmsService.sendFormDeactivatedSms, + ).toHaveBeenCalledWith( + form.admin.email, + String(form.admin._id), + form._id, + form.title, + MOCK_CONTACT.contact, + MOCK_CONTACT.email, + ) + expect( + MockedPostmanSmsService.sendFormDeactivatedSms, + ).toHaveBeenCalledWith( + form.admin.email, + String(form.admin._id), + form._id, + form.title, + MOCK_CONTACT_2.contact, + MOCK_CONTACT_2.email, + ) }) it('should return true even when some SMSes fail', async () => { @@ -870,7 +891,7 @@ describe('BounceService', () => { admin: testUser._id, title: MOCK_FORM_TITLE, }).populate('admin')) as IPopulatedForm - MockSmsFactory.sendFormDeactivatedSms + MockedPostmanSmsService.sendFormDeactivatedSms .mockReturnValueOnce(okAsync(true)) .mockReturnValueOnce(errAsync(new SmsSendError())) @@ -880,23 +901,29 @@ describe('BounceService', () => { ]) expect(result._unsafeUnwrap()).toEqual(true) - expect(MockSmsFactory.sendFormDeactivatedSms).toHaveBeenCalledTimes(2) - expect(MockSmsFactory.sendFormDeactivatedSms).toHaveBeenCalledWith({ - adminEmail: form.admin.email, - adminId: String(form.admin._id), - formId: form._id, - formTitle: form.title, - recipient: MOCK_CONTACT.contact, - recipientEmail: MOCK_CONTACT.email, - }) - expect(MockSmsFactory.sendFormDeactivatedSms).toHaveBeenCalledWith({ - adminEmail: form.admin.email, - adminId: String(form.admin._id), - formId: form._id, - formTitle: form.title, - recipient: MOCK_CONTACT_2.contact, - recipientEmail: MOCK_CONTACT_2.email, - }) + expect( + MockedPostmanSmsService.sendFormDeactivatedSms, + ).toHaveBeenCalledTimes(2) + expect( + MockedPostmanSmsService.sendFormDeactivatedSms, + ).toHaveBeenCalledWith( + form.admin.email, + String(form.admin._id), + form._id, + form.title, + MOCK_CONTACT.contact, + MOCK_CONTACT.email, + ) + expect( + MockedPostmanSmsService.sendFormDeactivatedSms, + ).toHaveBeenCalledWith( + form.admin.email, + String(form.admin._id), + form._id, + form.title, + MOCK_CONTACT_2.contact, + MOCK_CONTACT_2.email, + ) }) }) }) diff --git a/src/app/modules/bounce/bounce.errors.ts b/src/app/modules/bounce/bounce.errors.ts index 3b513c52b4..5e067ce695 100644 --- a/src/app/modules/bounce/bounce.errors.ts +++ b/src/app/modules/bounce/bounce.errors.ts @@ -1,4 +1,7 @@ -import { InvalidNumberError, SmsSendError } from '../../services/sms/sms.errors' +import { + InvalidNumberError, + SmsSendError, +} from '../../services/postman-sms/postman-sms.errors' import { ApplicationError } from '../core/core.errors' /** diff --git a/src/app/modules/bounce/bounce.service.ts b/src/app/modules/bounce/bounce.service.ts index 36972a273d..af45d296cb 100644 --- a/src/app/modules/bounce/bounce.service.ts +++ b/src/app/modules/bounce/bounce.service.ts @@ -16,7 +16,7 @@ import { } from '../../config/logger' import { EMAIL_HEADERS, EmailType } from '../../services/mail/mail.constants' import MailService from '../../services/mail/mail.service' -import { SmsFactory } from '../../services/sms/sms.factory' +import PostmanSmsService from '../../services/postman-sms/postman-sms.service' import { transformMongoError } from '../../utils/handle-mongo-error' import { PossibleDatabaseError } from '../core/core.errors' import { getCollabEmailsWithPermission } from '../form/form.utils' @@ -219,14 +219,14 @@ export const sendSmsBounceNotification = ( // empty array as list of recipients. ): ResultAsync => { const smsResults = possibleSmsRecipients.map((recipient) => - SmsFactory.sendBouncedSubmissionSms({ - adminEmail: form.admin.email, - adminId: String(form.admin._id), - formId: form._id, - formTitle: form.title, - recipient: recipient.contact, - recipientEmail: recipient.email, - }) + PostmanSmsService.sendBouncedSubmissionSms( + form.admin.email, + String(form.admin._id), + form._id, + form.title, + recipient.contact, + recipient.email, + ) .map(() => recipient) .mapErr( (error) => new SendBounceSmsNotificationError(error, recipient.contact), @@ -368,14 +368,14 @@ export const notifyAdminsOfDeactivation = ( // Best-effort attempt to send SMSes, don't propagate error upwards ): ResultAsync => { const smsResults = possibleSmsRecipients.map((recipient) => - SmsFactory.sendFormDeactivatedSms({ - adminEmail: form.admin.email, - adminId: String(form.admin._id), - formId: form._id, - formTitle: form.title, - recipient: recipient.contact, - recipientEmail: recipient.email, - }), + PostmanSmsService.sendFormDeactivatedSms( + form.admin.email, + String(form.admin._id), + form._id, + form.title, + recipient.contact, + recipient.email, + ), ) return ResultAsync.combineWithAllErrors(smsResults) .map(() => true as const) diff --git a/src/app/modules/user/__tests__/user.controller.spec.ts b/src/app/modules/user/__tests__/user.controller.spec.ts index c352059c58..258bed9787 100644 --- a/src/app/modules/user/__tests__/user.controller.spec.ts +++ b/src/app/modules/user/__tests__/user.controller.spec.ts @@ -9,8 +9,8 @@ import { MissingUserError, } from 'src/app/modules/user/user.errors' import * as UserService from 'src/app/modules/user/user.service' -import { SmsSendError } from 'src/app/services/sms/sms.errors' -import { SmsFactory } from 'src/app/services/sms/sms.factory' +import { SmsSendError } from 'src/app/services/postman-sms/postman-sms.errors' +import PostmanSmsService from 'src/app/services/postman-sms/postman-sms.service' import { HashingError } from 'src/app/utils/hash' import { IPopulatedUser } from 'src/types' @@ -18,9 +18,9 @@ import { DatabaseError } from '../../core/core.errors' import { UNAUTHORIZED_USER_MESSAGE } from '../user.constant' jest.mock('src/app/modules/user/user.service') -jest.mock('src/app/services/sms/sms.factory') +jest.mock('src/app/services/postman-sms/postman-sms.service') const MockUserService = jest.mocked(UserService) -const MockSmsFactory = jest.mocked(SmsFactory) +const MockPostmanSmsService = jest.mocked(PostmanSmsService) describe('user.controller', () => { afterEach(() => { @@ -49,7 +49,9 @@ describe('user.controller', () => { // Mock UserService and SmsFactory to pass without errors. MockUserService.createContactOtp.mockReturnValueOnce(okAsync(expectedOtp)) - MockSmsFactory.sendAdminContactOtp.mockReturnValueOnce(okAsync(true)) + MockPostmanSmsService.sendAdminContactOtp.mockReturnValueOnce( + okAsync(true), + ) // Act await UserController._handleContactSendOtp(MOCK_REQ, mockRes, jest.fn()) @@ -60,7 +62,7 @@ describe('user.controller', () => { MOCK_REQ.body.userId, MOCK_REQ.body.contact, ) - expect(MockSmsFactory.sendAdminContactOtp).toHaveBeenCalledWith( + expect(MockPostmanSmsService.sendAdminContactOtp).toHaveBeenCalledWith( MOCK_REQ.body.contact, expectedOtp, MOCK_REQ.body.userId, @@ -134,7 +136,7 @@ describe('user.controller', () => { // Mock UserService to pass without errors. MockUserService.createContactOtp.mockReturnValueOnce(okAsync('123456')) // Mock SmsFactory to return error. - MockSmsFactory.sendAdminContactOtp.mockReturnValueOnce( + MockPostmanSmsService.sendAdminContactOtp.mockReturnValueOnce( errAsync(new SmsSendError(mockErrorString)), ) diff --git a/src/app/modules/user/user.controller.ts b/src/app/modules/user/user.controller.ts index c72fddc1a2..67ec1bd1ad 100644 --- a/src/app/modules/user/user.controller.ts +++ b/src/app/modules/user/user.controller.ts @@ -7,7 +7,7 @@ import { } from '../../../../shared/types' import { IPopulatedUser } from '../../../types' import { createLoggerWithLabel } from '../../config/logger' -import { SmsFactory } from '../../services/sms/sms.factory' +import PostmanSmsService from '../../services/postman-sms/postman-sms.service' import { getRequestIp } from '../../utils/request' import { getUserIdFromSession } from '../auth/auth.utils' import { ControllerHandler } from '../core/core.types' @@ -70,7 +70,7 @@ export const _handleContactSendOtp: ControllerHandler< // Step 2: No error, send verification OTP to contact. const otp = createResult.value - const sendOtpResult = await SmsFactory.sendAdminContactOtp( + const sendOtpResult = await PostmanSmsService.sendAdminContactOtp( contact, otp, userId, diff --git a/src/app/modules/user/user.utils.ts b/src/app/modules/user/user.utils.ts index 386f373670..d27323bbc7 100644 --- a/src/app/modules/user/user.utils.ts +++ b/src/app/modules/user/user.utils.ts @@ -2,7 +2,7 @@ import { StatusCodes } from 'http-status-codes' import { UserContactView } from '../../../types' import { createLoggerWithLabel } from '../../config/logger' -import * as SmsErrors from '../../services/sms/sms.errors' +import * as SmsErrors from '../../services/postman-sms/postman-sms.errors' import { HashingError } from '../../utils/hash' import * as CoreErrors from '../core/core.errors' import { ErrorResponseData } from '../core/core.types' diff --git a/src/app/modules/verification/__tests__/verification.controller.spec.ts b/src/app/modules/verification/__tests__/verification.controller.spec.ts index 685aba9c20..8a73e15bf7 100644 --- a/src/app/modules/verification/__tests__/verification.controller.spec.ts +++ b/src/app/modules/verification/__tests__/verification.controller.spec.ts @@ -14,7 +14,7 @@ import { MailSendError } from 'src/app/services/mail/mail.errors' import { InvalidNumberError, SmsSendError, -} from 'src/app/services/sms/sms.errors' +} from 'src/app/services/postman-sms/postman-sms.errors' import { HashingError } from 'src/app/utils/hash' import * as OtpUtils from 'src/app/utils/otp' import { diff --git a/src/app/modules/verification/__tests__/verification.service.spec.ts b/src/app/modules/verification/__tests__/verification.service.spec.ts index eda4dee7e8..8a612e1a58 100644 --- a/src/app/modules/verification/__tests__/verification.service.spec.ts +++ b/src/app/modules/verification/__tests__/verification.service.spec.ts @@ -30,8 +30,8 @@ import { } from 'src/app/modules/verification/verification.errors' import { MailSendError } from 'src/app/services/mail/mail.errors' import MailService from 'src/app/services/mail/mail.service' +import { SmsSendError } from 'src/app/services/postman-sms/postman-sms.errors' import PostmanSmsService from 'src/app/services/postman-sms/postman-sms.service' -import { SmsSendError } from 'src/app/services/sms/sms.errors' import { SmsFactory } from 'src/app/services/sms/sms.factory' import * as HashUtils from 'src/app/utils/hash' import { IFormSchema, IVerificationSchema, UpdateFieldData } from 'src/types' diff --git a/src/app/modules/verification/verification.service.ts b/src/app/modules/verification/verification.service.ts index 5937e2227c..d3928bfbbb 100644 --- a/src/app/modules/verification/verification.service.ts +++ b/src/app/modules/verification/verification.service.ts @@ -17,8 +17,11 @@ import formsgSdk from '../../config/formsg-sdk' import { createLoggerWithLabel } from '../../config/logger' import { MailSendError } from '../../services/mail/mail.errors' import MailService from '../../services/mail/mail.service' +import { + InvalidNumberError, + SmsSendError, +} from '../../services/postman-sms/postman-sms.errors' import PostmanSmsService from '../../services/postman-sms/postman-sms.service' -import { InvalidNumberError, SmsSendError } from '../../services/sms/sms.errors' import { SmsFactory } from '../../services/sms/sms.factory' import { transformMongoError } from '../../utils/handle-mongo-error' import { compareHash, HashingError } from '../../utils/hash' diff --git a/src/app/modules/verification/verification.util.ts b/src/app/modules/verification/verification.util.ts index 1d13b3052e..7b1958ea7f 100644 --- a/src/app/modules/verification/verification.util.ts +++ b/src/app/modules/verification/verification.util.ts @@ -22,7 +22,10 @@ import { SmsLimitExceededError, } from '../../modules/verification/verification.errors' import { MailSendError } from '../../services/mail/mail.errors' -import { InvalidNumberError, SmsSendError } from '../../services/sms/sms.errors' +import { + InvalidNumberError, + SmsSendError, +} from '../../services/postman-sms/postman-sms.errors' import { HashingError } from '../../utils/hash' import { DatabaseConflictError, diff --git a/src/app/routes/api/v3/forms/__tests__/public-forms.verification.routes.spec.ts b/src/app/routes/api/v3/forms/__tests__/public-forms.verification.routes.spec.ts index f915ce3623..a0baa88ff0 100644 --- a/src/app/routes/api/v3/forms/__tests__/public-forms.verification.routes.spec.ts +++ b/src/app/routes/api/v3/forms/__tests__/public-forms.verification.routes.spec.ts @@ -22,7 +22,7 @@ import { } from 'src/app/modules/verification/__tests__/verification.test.helpers' import getVerificationModel from 'src/app/modules/verification/verification.model' import MailService from 'src/app/services/mail/mail.service' -import { SmsSendError } from 'src/app/services/sms/sms.errors' +import { SmsSendError } from 'src/app/services/postman-sms/postman-sms.errors' import * as SmsService from 'src/app/services/sms/sms.service' import * as OtpUtils from 'src/app/utils/otp' import { IVerificationSchema } from 'src/types' diff --git a/src/app/routes/api/v3/user/__tests__/user.routes.spec.ts b/src/app/routes/api/v3/user/__tests__/user.routes.spec.ts index c714d8410a..d8b58a2b6e 100644 --- a/src/app/routes/api/v3/user/__tests__/user.routes.spec.ts +++ b/src/app/routes/api/v3/user/__tests__/user.routes.spec.ts @@ -10,8 +10,8 @@ import supertest, { Session } from 'supertest-session' import getUserModel from 'src/app/models/user.server.model' import { UNAUTHORIZED_USER_MESSAGE } from 'src/app/modules/user/user.constant' -import { SmsSendError } from 'src/app/services/sms/sms.errors' -import * as SmsService from 'src/app/services/sms/sms.service' +import { SmsSendError } from 'src/app/services/postman-sms/postman-sms.errors' +import PostmanSmsService from 'src/app/services/postman-sms/postman-sms.service' import * as OtpUtils from 'src/app/utils/otp' import { AgencyDocument, IUserSchema } from 'src/types' @@ -113,7 +113,7 @@ describe('user.routes', () => { // Arrange const session = await createAuthedSession(defaultUser.email, request) const sendSmsOtpSpy = jest - .spyOn(SmsService, 'sendAdminContactOtp') + .spyOn(PostmanSmsService, 'sendAdminContactOtp') .mockReturnValueOnce(okAsync(true)) // Act @@ -205,7 +205,7 @@ describe('user.routes', () => { const mockErrorString = 'mock sms send error! oh no' const session = await createAuthedSession(defaultUser.email, request) const sendSmsOtpSpy = jest - .spyOn(SmsService, 'sendAdminContactOtp') + .spyOn(PostmanSmsService, 'sendAdminContactOtp') .mockReturnValueOnce(errAsync(new SmsSendError(mockErrorString))) // Act @@ -613,7 +613,7 @@ const requestForContactOtp = async ( ) => { // Set that so no real mail is sent. const sendSmsOtpSpy = jest - .spyOn(SmsService, 'sendAdminContactOtp') + .spyOn(PostmanSmsService, 'sendAdminContactOtp') .mockReturnValueOnce(okAsync(true)) // Act diff --git a/src/app/services/postman-sms/__tests__/postman-sms.service.spec.ts b/src/app/services/postman-sms/__tests__/postman-sms.service.spec.ts index db6a497984..e5f41f6b12 100644 --- a/src/app/services/postman-sms/__tests__/postman-sms.service.spec.ts +++ b/src/app/services/postman-sms/__tests__/postman-sms.service.spec.ts @@ -1,4 +1,5 @@ import dbHandler from '__tests__/unit/backend/helpers/jest-db' +import { ObjectId } from 'bson' import mongoose from 'mongoose' import { okAsync } from 'neverthrow' @@ -14,6 +15,11 @@ const FormModel = getFormModel(mongoose) const TEST_NUMBER = '+15005550006' +const MOCK_RECIPIENT_EMAIL = 'recipientEmail@email.com' +const MOCK_ADMIN_EMAIL = 'adminEmail@email.com' +const MOCK_ADMIN_ID = new ObjectId().toHexString() +const MOCK_FORM_ID = new ObjectId().toHexString() +const MOCK_FORM_TITLE = 'formTitle' const MOCK_SENDER_IP = '200.000.000.000' describe('postman-sms.service', () => { @@ -28,179 +34,115 @@ describe('postman-sms.service', () => { afterEach(async () => await dbHandler.clearDatabase()) afterAll(async () => await dbHandler.closeDatabase()) - // describe('sendFormDeactivatedSms', () => { - // it('should send SMS and log success when sending is successful', async () => { - // const expectedMessage = renderFormDeactivatedSms(MOCK_FORM_TITLE) - - // await PostmanSmsService.sendFormDeactivatedSms( - // { - // recipient: TWILIO_TEST_NUMBER, - // adminEmail: MOCK_ADMIN_EMAIL, - // adminId: MOCK_ADMIN_ID, - // formId: MOCK_FORM_ID, - // formTitle: MOCK_FORM_TITLE, - // recipientEmail: MOCK_RECIPIENT_EMAIL, - // }, - // MOCK_VALID_CONFIG, - // ) - - // expect(twilioSuccessSpy).toHaveBeenCalledWith({ - // to: TWILIO_TEST_NUMBER, - // body: expectedMessage, - // from: MOCK_VALID_CONFIG.msgSrvcSid, - // forceDelivery: true, - // statusCallback: expect.stringContaining(MOCK_TWILIO_WEBHOOK_ROUTE), - // }) - - // expect(twilioSuccessSpy.mock.calls[0][0].statusCallback).toEqual( - // expect.not.stringContaining('?senderIp'), - // ) - - // expect(smsCountSpy).toHaveBeenCalledWith({ - // smsData: { - // form: MOCK_FORM_ID, - // collaboratorEmail: MOCK_RECIPIENT_EMAIL, - // recipientNumber: TWILIO_TEST_NUMBER, - // formAdmin: { - // email: MOCK_ADMIN_EMAIL, - // userId: MOCK_ADMIN_ID, - // }, - // }, - // smsType: SmsType.DeactivatedForm, - // msgSrvcSid: MOCK_VALID_CONFIG.msgSrvcSid, - // logType: LogType.success, - // }) - // }) - - // it('should log failure when sending fails', async () => { - // // Arrange - // const expectedMessage = renderFormDeactivatedSms(MOCK_FORM_TITLE) - - // // Act - // const actualResult = await SmsService.sendFormDeactivatedSms( - // { - // recipient: TWILIO_TEST_NUMBER, - // adminEmail: MOCK_ADMIN_EMAIL, - // adminId: MOCK_ADMIN_ID, - // formId: MOCK_FORM_ID, - // formTitle: MOCK_FORM_TITLE, - // recipientEmail: MOCK_RECIPIENT_EMAIL, - // }, - // MOCK_INVALID_CONFIG, - // ) - - // // Assert - // expect(actualResult._unsafeUnwrapErr()).toEqual(new InvalidNumberError()) - // expect(twilioFailureSpy).toHaveBeenCalledWith({ - // to: TWILIO_TEST_NUMBER, - // body: expectedMessage, - // from: MOCK_INVALID_CONFIG.msgSrvcSid, - // forceDelivery: true, - // statusCallback: expect.stringContaining(MOCK_TWILIO_WEBHOOK_ROUTE), - // }) - // expect(smsCountSpy).toHaveBeenCalledWith({ - // smsData: { - // form: MOCK_FORM_ID, - // collaboratorEmail: MOCK_RECIPIENT_EMAIL, - // recipientNumber: TWILIO_TEST_NUMBER, - // formAdmin: { - // email: MOCK_ADMIN_EMAIL, - // userId: MOCK_ADMIN_ID, - // }, - // }, - // smsType: SmsType.DeactivatedForm, - // msgSrvcSid: MOCK_INVALID_CONFIG.msgSrvcSid, - // logType: LogType.failure, - // }) - // }) - // }) - - // describe('sendBouncedSubmissionSms', () => { - // it('should send SMS and log success when sending is successful', async () => { - // const expectedMessage = renderBouncedSubmissionSms(MOCK_FORM_TITLE) - - // await SmsService.sendBouncedSubmissionSms( - // { - // recipient: TWILIO_TEST_NUMBER, - // adminEmail: MOCK_ADMIN_EMAIL, - // adminId: MOCK_ADMIN_ID, - // formId: MOCK_FORM_ID, - // formTitle: MOCK_FORM_TITLE, - // recipientEmail: MOCK_RECIPIENT_EMAIL, - // }, - // MOCK_VALID_CONFIG, - // ) - - // expect(twilioSuccessSpy).toHaveBeenCalledWith({ - // to: TWILIO_TEST_NUMBER, - // body: expectedMessage, - // from: MOCK_VALID_CONFIG.msgSrvcSid, - // forceDelivery: true, - // statusCallback: expect.stringContaining(MOCK_TWILIO_WEBHOOK_ROUTE), - // }) - - // expect(twilioSuccessSpy.mock.calls[0][0].statusCallback).toEqual( - // expect.not.stringContaining('?senderIp'), - // ) - - // expect(smsCountSpy).toHaveBeenCalledWith({ - // smsData: { - // form: MOCK_FORM_ID, - // collaboratorEmail: MOCK_RECIPIENT_EMAIL, - // recipientNumber: TWILIO_TEST_NUMBER, - // formAdmin: { - // email: MOCK_ADMIN_EMAIL, - // userId: MOCK_ADMIN_ID, - // }, - // }, - // smsType: SmsType.BouncedSubmission, - // msgSrvcSid: MOCK_VALID_CONFIG.msgSrvcSid, - // logType: LogType.success, - // }) - // }) - - // it('should log failure when sending fails', async () => { - // // Arrange - // const expectedMessage = renderBouncedSubmissionSms(MOCK_FORM_TITLE) - - // // Act - // const actualResult = await SmsService.sendBouncedSubmissionSms( - // { - // recipient: TWILIO_TEST_NUMBER, - // adminEmail: MOCK_ADMIN_EMAIL, - // adminId: MOCK_ADMIN_ID, - // formId: MOCK_FORM_ID, - // formTitle: MOCK_FORM_TITLE, - // recipientEmail: MOCK_RECIPIENT_EMAIL, - // }, - // MOCK_INVALID_CONFIG, - // ) - - // // Assert - // expect(actualResult._unsafeUnwrapErr()).toEqual(new InvalidNumberError()) - // expect(twilioFailureSpy).toHaveBeenCalledWith({ - // to: TWILIO_TEST_NUMBER, - // body: expectedMessage, - // from: MOCK_INVALID_CONFIG.msgSrvcSid, - // forceDelivery: true, - // statusCallback: expect.stringContaining(MOCK_TWILIO_WEBHOOK_ROUTE), - // }) - // expect(smsCountSpy).toHaveBeenCalledWith({ - // smsData: { - // form: MOCK_FORM_ID, - // collaboratorEmail: MOCK_RECIPIENT_EMAIL, - // recipientNumber: TWILIO_TEST_NUMBER, - // formAdmin: { - // email: MOCK_ADMIN_EMAIL, - // userId: MOCK_ADMIN_ID, - // }, - // }, - // smsType: SmsType.BouncedSubmission, - // msgSrvcSid: MOCK_INVALID_CONFIG.msgSrvcSid, - // logType: LogType.failure, - // }) - // }) - // }) + describe('sendFormDeactivatedSms', () => { + it('should send SMS through internal channel when sending is successful', async () => { + // Arrange + const postmanInternalSendSpy = jest + .spyOn(PostmanSmsService, '_sendInternalSms') + .mockResolvedValueOnce(okAsync(true)) + + const postmanMopSendSpy = jest + .spyOn(PostmanSmsService, '_sendMopSms') + .mockResolvedValueOnce(okAsync(true)) + + // Act + await PostmanSmsService.sendFormDeactivatedSms( + TEST_NUMBER, + MOCK_ADMIN_EMAIL, + MOCK_ADMIN_ID, + MOCK_FORM_ID, + MOCK_FORM_TITLE, + MOCK_RECIPIENT_EMAIL, + ) + + // Assert + expect(postmanInternalSendSpy).toHaveBeenCalledOnce() + expect(postmanMopSendSpy).not.toHaveBeenCalled() + }) + + it('should return InvalidNumberError when invalid number is supplied', async () => { + // Arrange + const postmanInternalSendSpy = jest + .spyOn(PostmanSmsService, '_sendInternalSms') + .mockResolvedValueOnce(okAsync(true)) + + const postmanMopSendSpy = jest + .spyOn(PostmanSmsService, '_sendMopSms') + .mockResolvedValueOnce(okAsync(true)) + + const invalidNumber = '1+11' + + // Act + const actualResult = await PostmanSmsService.sendFormDeactivatedSms( + invalidNumber, + MOCK_ADMIN_EMAIL, + MOCK_ADMIN_ID, + MOCK_FORM_ID, + MOCK_FORM_TITLE, + MOCK_RECIPIENT_EMAIL, + ) + + // Assert + expect(actualResult._unsafeUnwrapErr()).toEqual(new InvalidNumberError()) + expect(postmanInternalSendSpy).not.toHaveBeenCalled() + expect(postmanMopSendSpy).not.toHaveBeenCalled() + }) + }) + + describe('sendBouncedSubmissionSms', () => { + it('should send SMS through internal channel success when sending is successful', async () => { + // Arrange + const postmanInternalSendSpy = jest + .spyOn(PostmanSmsService, '_sendInternalSms') + .mockResolvedValueOnce(okAsync(true)) + + const postmanMopSendSpy = jest + .spyOn(PostmanSmsService, '_sendMopSms') + .mockResolvedValueOnce(okAsync(true)) + + // Act + await PostmanSmsService.sendBouncedSubmissionSms( + TEST_NUMBER, + MOCK_ADMIN_EMAIL, + MOCK_ADMIN_ID, + MOCK_FORM_ID, + MOCK_FORM_TITLE, + MOCK_RECIPIENT_EMAIL, + ) + + expect(postmanInternalSendSpy).toHaveBeenCalledOnce() + expect(postmanMopSendSpy).not.toHaveBeenCalled() + }) + + it('should return InvalidNumberError when invalid number is supplied', async () => { + // Arrange + const postmanInternalSendSpy = jest + .spyOn(PostmanSmsService, '_sendInternalSms') + .mockResolvedValueOnce(okAsync(true)) + + const postmanMopSendSpy = jest + .spyOn(PostmanSmsService, '_sendMopSms') + .mockResolvedValueOnce(okAsync(true)) + + const invalidNumber = '1+11' + + // Act + const actualResult = await PostmanSmsService.sendBouncedSubmissionSms( + invalidNumber, + MOCK_ADMIN_EMAIL, + MOCK_ADMIN_ID, + MOCK_FORM_ID, + MOCK_FORM_TITLE, + MOCK_RECIPIENT_EMAIL, + ) + + // Assert + expect(actualResult._unsafeUnwrapErr()).toEqual(new InvalidNumberError()) + + expect(postmanInternalSendSpy).not.toHaveBeenCalled() + expect(postmanMopSendSpy).not.toHaveBeenCalled() + }) + }) describe('sendVerificationOtp', () => { let mockOtpData: FormOtpData @@ -228,7 +170,7 @@ describe('postman-sms.service', () => { // Return null on Form method jest.spyOn(FormModel, 'getOtpData').mockResolvedValueOnce(null) const postmanSendSpy = jest - .spyOn(PostmanSmsService, 'sendMopSms') + .spyOn(PostmanSmsService, '_sendMopSms') .mockResolvedValueOnce(okAsync(true)) // Act @@ -254,7 +196,7 @@ describe('postman-sms.service', () => { jest.spyOn(FormModel, 'getOtpData').mockResolvedValueOnce(mockOtpData) const postmanSendSpy = jest - .spyOn(PostmanSmsService, 'sendMopSms') + .spyOn(PostmanSmsService, '_sendMopSms') .mockResolvedValueOnce(okAsync(true)) // Act @@ -271,11 +213,11 @@ describe('postman-sms.service', () => { expect(postmanSendSpy).toHaveBeenCalledOnce() }) - it('should return InvalidNumberError when verification OTP fails to send due to invalid number', async () => { + it('should return InvalidNumberError when invalid number is supplied', async () => { // Arrange jest.spyOn(FormModel, 'getOtpData').mockResolvedValueOnce(mockOtpData) const postmanSendSpy = jest - .spyOn(PostmanSmsService, 'sendMopSms') + .spyOn(PostmanSmsService, '_sendMopSms') .mockResolvedValueOnce(okAsync(true)) const invalidNumber = '1+11123' @@ -294,93 +236,55 @@ describe('postman-sms.service', () => { }) }) - // describe('sendAdminContactOtp', () => { - // it('should log and send contact OTP when sending has no errors', async () => { - // // Act - // const actualResult = await SmsService.sendAdminContactOtp( - // /* recipient= */ TWILIO_TEST_NUMBER, - // /* otp= */ '111111', - // /* userId= */ testUser._id, - // /* senderIp= */ MOCK_SENDER_IP, - // /* defaultConfig= */ MOCK_VALID_CONFIG, - // ) - - // // Assert - // expect(twilioSuccessSpy.mock.calls[0][0].statusCallback).toEqual( - // expect.stringContaining('?senderIp'), - // ) - - // // Should resolve to true - // expect(actualResult.isOk()).toEqual(true) - // expect(actualResult._unsafeUnwrap()).toEqual(true) - // // Logging should also have happened. - // const expectedLogParams = { - // smsData: { - // admin: testUser._id, - // }, - // msgSrvcSid: MOCK_MSG_SRVC_SID, - // smsType: SmsType.AdminContact, - // logType: LogType.success, - // } - // expect(smsCountSpy).toHaveBeenCalledWith(expectedLogParams) - // }) - // }) - - // describe('retrieveFreeSmsCounts', () => { - // const VERIFICATION_SMS_COUNT = 3 - - // it('should retrieve sms counts correctly for a specified user', async () => { - // // Arrange - // const retrieveSpy = jest.spyOn(SmsCountModel, 'retrieveFreeSmsCounts') - // retrieveSpy.mockResolvedValueOnce(VERIFICATION_SMS_COUNT) - - // // Act - // const actual = await SmsService.retrieveFreeSmsCounts(testUser._id) - - // // Assert - // expect(actual._unsafeUnwrap()).toBe(VERIFICATION_SMS_COUNT) - // }) - - // it('should return a database error when retrieval fails', async () => { - // // Arrange - // const retrieveSpy = jest.spyOn(SmsCountModel, 'retrieveFreeSmsCounts') - // retrieveSpy.mockRejectedValueOnce('ohno') - - // // Act - // const actual = await SmsService.retrieveFreeSmsCounts(testUser._id) - - // // Assert - // expect(actual._unsafeUnwrapErr()).toEqual( - // new DatabaseError(getMongoErrorMessage('ohno')), - // ) - // }) - // }) - - // it('should log failure and throw error when contact OTP fails to send', async () => { - // // Act - // const actualResult = await SmsService.sendAdminContactOtp( - // /* recipient= */ TWILIO_TEST_NUMBER, - // /* otp= */ '111111', - // /* userId= */ testUser._id, - // /* senderIp= */ MOCK_SENDER_IP, - // /* defaultConfig= */ MOCK_INVALID_CONFIG, - // ) - - // // Assert - // const expectedError = new Error(VfnErrors.InvalidMobileNumber) - // expectedError.name = VfnErrors.SendOtpFailed - - // expect(actualResult.isErr()).toEqual(true) - - // // Logging should also have happened. - // const expectedLogParams = { - // smsData: { - // admin: testUser._id, - // }, - // msgSrvcSid: MOCK_MSG_SRVC_SID, - // smsType: SmsType.AdminContact, - // logType: LogType.failure, - // } - // expect(smsCountSpy).toHaveBeenCalledWith(expectedLogParams) - // }) + describe('sendAdminContactOtp', () => { + it('should log and send contact OTP when sending has no errors', async () => { + // Arrange + const postmanInternalSendSpy = jest + .spyOn(PostmanSmsService, '_sendInternalSms') + .mockResolvedValueOnce(okAsync(true)) + + const postmanMopSendSpy = jest + .spyOn(PostmanSmsService, '_sendMopSms') + .mockResolvedValueOnce(okAsync(true)) + + // Act + const actualResult = await PostmanSmsService.sendAdminContactOtp( + TEST_NUMBER, + '111111', + testUser._id, + MOCK_SENDER_IP, + ) + + // Assert + expect(actualResult._unsafeUnwrap()).toEqual(true) + expect(postmanInternalSendSpy).toHaveBeenCalledOnce() + expect(postmanMopSendSpy).not.toHaveBeenCalled() + }) + + it('should return InvalidNumberError when invalid number is supplied', async () => { + // Arrange + const postmanInternalSendSpy = jest + .spyOn(PostmanSmsService, '_sendInternalSms') + .mockResolvedValueOnce(okAsync(true)) + + const postmanMopSendSpy = jest + .spyOn(PostmanSmsService, '_sendMopSms') + .mockResolvedValueOnce(okAsync(true)) + + const invalidNumber = '1+11123' + + // Act + const actualResult = await PostmanSmsService.sendAdminContactOtp( + invalidNumber, + '111111', + testUser._id, + MOCK_SENDER_IP, + ) + + // Assert + expect(actualResult._unsafeUnwrapErr()).toEqual(new InvalidNumberError()) + expect(postmanInternalSendSpy).not.toHaveBeenCalled() + expect(postmanMopSendSpy).not.toHaveBeenCalled() + }) + }) }) diff --git a/src/app/services/postman-sms/postman-sms.service.ts b/src/app/services/postman-sms/postman-sms.service.ts index 1f96b10114..a14eb70c9d 100644 --- a/src/app/services/postman-sms/postman-sms.service.ts +++ b/src/app/services/postman-sms/postman-sms.service.ts @@ -3,7 +3,7 @@ import mongoose from 'mongoose' import { errAsync, okAsync, ResultAsync } from 'neverthrow' import { isPhoneNumber } from '../../../../shared/utils/phone-num-validation' -import { FormOtpData } from '../../../types' +import { AdminContactOtpData, FormOtpData } from '../../../types' import { useMockPostmanSms } from '../../config/config' import { postmanSmsConfig } from '../../config/features/postman-sms.config' import { createLoggerWithLabel } from '../../config/logger' @@ -16,8 +16,16 @@ import { getMongoErrorMessage } from '../../utils/handle-mongo-error' import MailService from '../mail/mail.service' import { InvalidNumberError, SmsSendError } from './postman-sms.errors' -import { SmsType } from './postman-sms.types' -import { renderVerificationSms } from './postman-sms.util' +import { + BouncedSubmissionSmsData, + FormDeactivatedSmsData, + SmsType, +} from './postman-sms.types' +import { + renderBouncedSubmissionSms, + renderFormDeactivatedSms, + renderVerificationSms, +} from './postman-sms.util' const logger = createLoggerWithLabel(module) const Form = getFormModel(mongoose) @@ -28,7 +36,7 @@ class PostmanSmsService { * SMSes will be sent using govsg sender id. * Messages to any member of public MUST be sent using this method. */ - sendMopSms( + _sendMopSms( smsData: FormOtpData, recipient: string, message: string, @@ -88,8 +96,62 @@ class PostmanSmsService { * * SMSes will be sent using FormSG sender id. */ - private sendInternalSms() { - // stub + _sendInternalSms( + smsData: + | FormDeactivatedSmsData + | BouncedSubmissionSmsData + | AdminContactOtpData, + recipient: string, + message: string, + smsType: SmsType, + senderIp?: string, + ): ResultAsync { + const logMeta = { + action: '_sendInternalSms', + recipient, + smsType, + smsData, + senderIp, + } + + const body = { + recipient: recipient.replace('+', ''), + language: 'english', + values: { body: message }, + } + + if (useMockPostmanSms) { + return MailService.sendLocalDevMail( + '[Mock Postman SMS] Captured SMS', + message, + ) + } + + const campaignUrl = + postmanSmsConfig.postmanBaseUrl + + `/campaigns/${postmanSmsConfig.internalCampaignId}/messages` + + return ResultAsync.fromPromise( + axios.post(campaignUrl, body, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${postmanSmsConfig.internalCampaignApiKey}`, + }, + }), + (error) => { + logger.warn({ + message: 'Error sending SMS using Postman API', + meta: { + ...logMeta, + postmanError: (error as AxiosError).response?.data, + }, + }) + + return new SmsSendError() + }, + ).andThen(() => { + return okAsync(true as const) + }) } /** @@ -147,7 +209,7 @@ class PostmanSmsService { } const message = renderVerificationSms(otp, otpPrefix) - return this.sendMopSms( + return this._sendMopSms( otpData, recipient, message, @@ -156,6 +218,137 @@ class PostmanSmsService { ) }) } + + public sendBouncedSubmissionSms( + recipient: string, + recipientEmail: string, + adminId: string, + adminEmail: string, + formId: string, + formTitle: string, + ): ResultAsync { + const logMeta = { + action: 'sendBouncedSubmissionSms', + formId, + } + + logger.info({ + message: `Sending bounced submission notification for ${recipientEmail}`, + meta: logMeta, + }) + + if (!isPhoneNumber(recipient)) { + logger.warn({ + message: `${recipient} is not a valid phone number`, + meta: logMeta, + }) + return errAsync(new InvalidNumberError()) + } + + const message = renderBouncedSubmissionSms(formTitle) + + const smsData: BouncedSubmissionSmsData = { + form: formId, + collaboratorEmail: recipientEmail, + recipientNumber: recipient, + formAdmin: { + email: adminEmail, + userId: adminId, + }, + } + + return this._sendInternalSms( + smsData, + recipient, + message, + SmsType.BouncedSubmission, + ) + } + + public sendFormDeactivatedSms( + recipient: string, + recipientEmail: string, + adminId: string, + adminEmail: string, + formId: string, + formTitle: string, + ): ResultAsync { + const logMeta = { + action: 'sendFormDeactivatedSms', + formId, + } + + logger.info({ + message: `Sending form deactivation notification for ${recipientEmail}`, + meta: logMeta, + }) + + if (!isPhoneNumber(recipient)) { + logger.warn({ + message: `${recipient} is not a valid phone number`, + meta: logMeta, + }) + return errAsync(new InvalidNumberError()) + } + + const message = renderFormDeactivatedSms(formTitle) + + const smsData: FormDeactivatedSmsData = { + form: formId, + collaboratorEmail: recipientEmail, + recipientNumber: recipient, + formAdmin: { + email: adminEmail, + userId: adminId, + }, + } + + return this._sendInternalSms( + smsData, + recipient, + message, + SmsType.DeactivatedForm, + ) + } + + public sendAdminContactOtp( + recipient: string, + otp: string, + userId: string, + senderIp: string, + ): ResultAsync { + const logMeta = { + action: 'sendAdminContactOtp', + userId, + } + + logger.info({ + message: `Sending admin contact verification OTP for ${userId}`, + meta: logMeta, + }) + + if (!isPhoneNumber(recipient)) { + logger.warn({ + message: `${recipient} is not a valid phone number`, + meta: logMeta, + }) + return errAsync(new InvalidNumberError()) + } + + const message = `Use the OTP ${otp} to verify your emergency contact number.` + + const otpData: AdminContactOtpData = { + admin: userId, + } + + return this._sendInternalSms( + otpData, + recipient, + message, + SmsType.AdminContact, + senderIp, + ) + } } export default new PostmanSmsService() diff --git a/src/app/services/postman-sms/postman-sms.types.ts b/src/app/services/postman-sms/postman-sms.types.ts index 945bccafa4..d95bf4fb5a 100644 --- a/src/app/services/postman-sms/postman-sms.types.ts +++ b/src/app/services/postman-sms/postman-sms.types.ts @@ -1,6 +1,21 @@ +import { FormPermission } from '../../../../shared/types' +import { IFormSchema, IUserSchema } from '../../../types' + export enum SmsType { Verification = 'VERIFICATION', AdminContact = 'ADMIN_CONTACT', DeactivatedForm = 'DEACTIVATED_FORM', BouncedSubmission = 'BOUNCED_SUBMISSION', } + +export type FormDeactivatedSmsData = { + form: IFormSchema['_id'] + formAdmin: { + email: IUserSchema['email'] + userId: IUserSchema['_id'] + } + collaboratorEmail: FormPermission['email'] + recipientNumber: string +} + +export type BouncedSubmissionSmsData = FormDeactivatedSmsData diff --git a/src/app/services/sms/__tests__/sms.factory.spec.ts b/src/app/services/sms/__tests__/sms.factory.spec.ts index 0714aaebec..812208febd 100644 --- a/src/app/services/sms/__tests__/sms.factory.spec.ts +++ b/src/app/services/sms/__tests__/sms.factory.spec.ts @@ -1,4 +1,3 @@ -import { ObjectId } from 'bson' import { okAsync } from 'neverthrow' import Twilio from 'twilio' @@ -6,7 +5,7 @@ import { ISms } from 'src/app/config/features/sms.config' import { createSmsFactory } from '../sms.factory' import * as SmsService from '../sms.service' -import { BounceNotificationSmsParams, TwilioConfig } from '../sms.types' +import { TwilioConfig } from '../sms.types' // This is hoisted and thus a const cannot be passed in. jest.mock('twilio', () => @@ -24,15 +23,6 @@ const MOCKED_TWILIO = { mocked: 'this is mocked', } as unknown as Twilio.Twilio -const MOCK_BOUNCE_SMS_PARAMS: BounceNotificationSmsParams = { - adminEmail: 'admin@email.com', - adminId: new ObjectId().toHexString(), - formId: new ObjectId().toHexString(), - formTitle: 'mock form title', - recipient: '+6581234567', - recipientEmail: 'recipient@email.com', -} - describe('sms.factory', () => { beforeEach(() => jest.clearAllMocks()) @@ -49,28 +39,6 @@ describe('sms.factory', () => { } const SmsFactory = createSmsFactory(MOCK_SMS_FEATURE) - it('should call SmsService counterpart when invoking sendAdminContactOtp', async () => { - // Arrange - MockSmsService.sendAdminContactOtp.mockReturnValueOnce(okAsync(true)) - - const mockArguments: Parameters = [ - 'mockRecipient', - 'mockOtp', - 'mockFormId', - 'mockSenderIp', - ] - - // Act - await SmsFactory.sendAdminContactOtp(...mockArguments) - - // Assert - expect(MockSmsService.sendAdminContactOtp).toHaveBeenCalledTimes(1) - expect(MockSmsService.sendAdminContactOtp).toHaveBeenCalledWith( - ...mockArguments, - expectedTwilioConfig, - ) - }) - it('should call SmsService counterpart when invoking sendVerificationOtp', async () => { // Arrange MockSmsService.sendVerificationOtp.mockReturnValue(okAsync(true)) @@ -93,26 +61,4 @@ describe('sms.factory', () => { expectedTwilioConfig, ) }) - - it('should call SmsService when sendFormDeactivatedSms is called', async () => { - MockSmsService.sendFormDeactivatedSms.mockReturnValue(okAsync(true)) - - await SmsFactory.sendFormDeactivatedSms(MOCK_BOUNCE_SMS_PARAMS) - - expect(MockSmsService.sendFormDeactivatedSms).toHaveBeenCalledWith( - MOCK_BOUNCE_SMS_PARAMS, - expectedTwilioConfig, - ) - }) - - it('should call SmsService when sendBouncedSubmissionSms is called', async () => { - MockSmsService.sendBouncedSubmissionSms.mockReturnValue(okAsync(true)) - - await SmsFactory.sendBouncedSubmissionSms(MOCK_BOUNCE_SMS_PARAMS) - - expect(MockSmsService.sendBouncedSubmissionSms).toHaveBeenCalledWith( - MOCK_BOUNCE_SMS_PARAMS, - expectedTwilioConfig, - ) - }) }) diff --git a/src/app/services/sms/__tests__/sms.service.spec.ts b/src/app/services/sms/__tests__/sms.service.spec.ts index 09bee43484..771bef9055 100644 --- a/src/app/services/sms/__tests__/sms.service.spec.ts +++ b/src/app/services/sms/__tests__/sms.service.spec.ts @@ -1,5 +1,4 @@ import dbHandler from '__tests__/unit/backend/helpers/jest-db' -import { ObjectId } from 'bson' import mongoose from 'mongoose' import getFormModel from 'src/app/models/form.server.model' @@ -11,14 +10,9 @@ import { getMongoErrorMessage } from 'src/app/utils/handle-mongo-error' import { FormOtpData, IFormSchema, IUserSchema } from 'src/types' import { FormResponseMode } from '../../../../../shared/types' -import { VfnErrors } from '../../../../../shared/utils/verification' -import { InvalidNumberError } from '../sms.errors' +import { InvalidNumberError } from '../../postman-sms/postman-sms.errors' import * as SmsService from '../sms.service' import { LogType, SmsType, TwilioConfig } from '../sms.types' -import { - renderBouncedSubmissionSms, - renderFormDeactivatedSms, -} from '../sms.util' import getSmsCountModel from '../sms_count.server.model' const FormModel = getFormModel(mongoose) @@ -28,15 +22,8 @@ const SmsCountModel = getSmsCountModel(mongoose) // https://www.twilio.com/docs/iam/test-credentials const TWILIO_TEST_NUMBER = '+15005550006' const MOCK_MSG_SRVC_SID = 'mockMsgSrvcSid' -const MOCK_RECIPIENT_EMAIL = 'recipientEmail@email.com' -const MOCK_ADMIN_EMAIL = 'adminEmail@email.com' -const MOCK_ADMIN_ID = new ObjectId().toHexString() -const MOCK_FORM_ID = new ObjectId().toHexString() -const MOCK_FORM_TITLE = 'formTitle' const MOCK_SENDER_IP = '200.000.000.000' -const MOCK_TWILIO_WEBHOOK_ROUTE = '/api/v3/notifications/twilio' - const twilioSuccessSpy = jest.fn().mockResolvedValue({ status: 'testStatus', sid: 'testSid', @@ -80,180 +67,6 @@ describe('sms.service', () => { afterEach(async () => await dbHandler.clearDatabase()) afterAll(async () => await dbHandler.closeDatabase()) - describe('sendFormDeactivatedSms', () => { - it('should send SMS and log success when sending is successful', async () => { - const expectedMessage = renderFormDeactivatedSms(MOCK_FORM_TITLE) - - await SmsService.sendFormDeactivatedSms( - { - recipient: TWILIO_TEST_NUMBER, - adminEmail: MOCK_ADMIN_EMAIL, - adminId: MOCK_ADMIN_ID, - formId: MOCK_FORM_ID, - formTitle: MOCK_FORM_TITLE, - recipientEmail: MOCK_RECIPIENT_EMAIL, - }, - MOCK_VALID_CONFIG, - ) - - expect(twilioSuccessSpy).toHaveBeenCalledWith({ - to: TWILIO_TEST_NUMBER, - body: expectedMessage, - from: MOCK_VALID_CONFIG.msgSrvcSid, - forceDelivery: true, - statusCallback: expect.stringContaining(MOCK_TWILIO_WEBHOOK_ROUTE), - }) - - expect(twilioSuccessSpy.mock.calls[0][0].statusCallback).toEqual( - expect.not.stringContaining('?senderIp'), - ) - - expect(smsCountSpy).toHaveBeenCalledWith({ - smsData: { - form: MOCK_FORM_ID, - collaboratorEmail: MOCK_RECIPIENT_EMAIL, - recipientNumber: TWILIO_TEST_NUMBER, - formAdmin: { - email: MOCK_ADMIN_EMAIL, - userId: MOCK_ADMIN_ID, - }, - }, - smsType: SmsType.DeactivatedForm, - msgSrvcSid: MOCK_VALID_CONFIG.msgSrvcSid, - logType: LogType.success, - }) - }) - - it('should log failure when sending fails', async () => { - // Arrange - const expectedMessage = renderFormDeactivatedSms(MOCK_FORM_TITLE) - - // Act - const actualResult = await SmsService.sendFormDeactivatedSms( - { - recipient: TWILIO_TEST_NUMBER, - adminEmail: MOCK_ADMIN_EMAIL, - adminId: MOCK_ADMIN_ID, - formId: MOCK_FORM_ID, - formTitle: MOCK_FORM_TITLE, - recipientEmail: MOCK_RECIPIENT_EMAIL, - }, - MOCK_INVALID_CONFIG, - ) - - // Assert - expect(actualResult._unsafeUnwrapErr()).toEqual(new InvalidNumberError()) - expect(twilioFailureSpy).toHaveBeenCalledWith({ - to: TWILIO_TEST_NUMBER, - body: expectedMessage, - from: MOCK_INVALID_CONFIG.msgSrvcSid, - forceDelivery: true, - statusCallback: expect.stringContaining(MOCK_TWILIO_WEBHOOK_ROUTE), - }) - expect(smsCountSpy).toHaveBeenCalledWith({ - smsData: { - form: MOCK_FORM_ID, - collaboratorEmail: MOCK_RECIPIENT_EMAIL, - recipientNumber: TWILIO_TEST_NUMBER, - formAdmin: { - email: MOCK_ADMIN_EMAIL, - userId: MOCK_ADMIN_ID, - }, - }, - smsType: SmsType.DeactivatedForm, - msgSrvcSid: MOCK_INVALID_CONFIG.msgSrvcSid, - logType: LogType.failure, - }) - }) - }) - - describe('sendBouncedSubmissionSms', () => { - it('should send SMS and log success when sending is successful', async () => { - const expectedMessage = renderBouncedSubmissionSms(MOCK_FORM_TITLE) - - await SmsService.sendBouncedSubmissionSms( - { - recipient: TWILIO_TEST_NUMBER, - adminEmail: MOCK_ADMIN_EMAIL, - adminId: MOCK_ADMIN_ID, - formId: MOCK_FORM_ID, - formTitle: MOCK_FORM_TITLE, - recipientEmail: MOCK_RECIPIENT_EMAIL, - }, - MOCK_VALID_CONFIG, - ) - - expect(twilioSuccessSpy).toHaveBeenCalledWith({ - to: TWILIO_TEST_NUMBER, - body: expectedMessage, - from: MOCK_VALID_CONFIG.msgSrvcSid, - forceDelivery: true, - statusCallback: expect.stringContaining(MOCK_TWILIO_WEBHOOK_ROUTE), - }) - - expect(twilioSuccessSpy.mock.calls[0][0].statusCallback).toEqual( - expect.not.stringContaining('?senderIp'), - ) - - expect(smsCountSpy).toHaveBeenCalledWith({ - smsData: { - form: MOCK_FORM_ID, - collaboratorEmail: MOCK_RECIPIENT_EMAIL, - recipientNumber: TWILIO_TEST_NUMBER, - formAdmin: { - email: MOCK_ADMIN_EMAIL, - userId: MOCK_ADMIN_ID, - }, - }, - smsType: SmsType.BouncedSubmission, - msgSrvcSid: MOCK_VALID_CONFIG.msgSrvcSid, - logType: LogType.success, - }) - }) - - it('should log failure when sending fails', async () => { - // Arrange - const expectedMessage = renderBouncedSubmissionSms(MOCK_FORM_TITLE) - - // Act - const actualResult = await SmsService.sendBouncedSubmissionSms( - { - recipient: TWILIO_TEST_NUMBER, - adminEmail: MOCK_ADMIN_EMAIL, - adminId: MOCK_ADMIN_ID, - formId: MOCK_FORM_ID, - formTitle: MOCK_FORM_TITLE, - recipientEmail: MOCK_RECIPIENT_EMAIL, - }, - MOCK_INVALID_CONFIG, - ) - - // Assert - expect(actualResult._unsafeUnwrapErr()).toEqual(new InvalidNumberError()) - expect(twilioFailureSpy).toHaveBeenCalledWith({ - to: TWILIO_TEST_NUMBER, - body: expectedMessage, - from: MOCK_INVALID_CONFIG.msgSrvcSid, - forceDelivery: true, - statusCallback: expect.stringContaining(MOCK_TWILIO_WEBHOOK_ROUTE), - }) - expect(smsCountSpy).toHaveBeenCalledWith({ - smsData: { - form: MOCK_FORM_ID, - collaboratorEmail: MOCK_RECIPIENT_EMAIL, - recipientNumber: TWILIO_TEST_NUMBER, - formAdmin: { - email: MOCK_ADMIN_EMAIL, - userId: MOCK_ADMIN_ID, - }, - }, - smsType: SmsType.BouncedSubmission, - msgSrvcSid: MOCK_INVALID_CONFIG.msgSrvcSid, - logType: LogType.failure, - }) - }) - }) - describe('sendVerificationOtp', () => { let mockOtpData: FormOtpData let testForm: IFormSchema @@ -355,38 +168,6 @@ describe('sms.service', () => { }) }) - describe('sendAdminContactOtp', () => { - it('should log and send contact OTP when sending has no errors', async () => { - // Act - const actualResult = await SmsService.sendAdminContactOtp( - /* recipient= */ TWILIO_TEST_NUMBER, - /* otp= */ '111111', - /* userId= */ testUser._id, - /* senderIp= */ MOCK_SENDER_IP, - /* defaultConfig= */ MOCK_VALID_CONFIG, - ) - - // Assert - expect(twilioSuccessSpy.mock.calls[0][0].statusCallback).toEqual( - expect.stringContaining('?senderIp'), - ) - - // Should resolve to true - expect(actualResult.isOk()).toEqual(true) - expect(actualResult._unsafeUnwrap()).toEqual(true) - // Logging should also have happened. - const expectedLogParams = { - smsData: { - admin: testUser._id, - }, - msgSrvcSid: MOCK_MSG_SRVC_SID, - smsType: SmsType.AdminContact, - logType: LogType.success, - } - expect(smsCountSpy).toHaveBeenCalledWith(expectedLogParams) - }) - }) - describe('retrieveFreeSmsCounts', () => { const VERIFICATION_SMS_COUNT = 3 @@ -416,32 +197,4 @@ describe('sms.service', () => { ) }) }) - - it('should log failure and throw error when contact OTP fails to send', async () => { - // Act - const actualResult = await SmsService.sendAdminContactOtp( - /* recipient= */ TWILIO_TEST_NUMBER, - /* otp= */ '111111', - /* userId= */ testUser._id, - /* senderIp= */ MOCK_SENDER_IP, - /* defaultConfig= */ MOCK_INVALID_CONFIG, - ) - - // Assert - const expectedError = new Error(VfnErrors.InvalidMobileNumber) - expectedError.name = VfnErrors.SendOtpFailed - - expect(actualResult.isErr()).toEqual(true) - - // Logging should also have happened. - const expectedLogParams = { - smsData: { - admin: testUser._id, - }, - msgSrvcSid: MOCK_MSG_SRVC_SID, - smsType: SmsType.AdminContact, - logType: LogType.failure, - } - expect(smsCountSpy).toHaveBeenCalledWith(expectedLogParams) - }) }) diff --git a/src/app/services/sms/sms.errors.ts b/src/app/services/sms/sms.errors.ts deleted file mode 100644 index 2f38ac4434..0000000000 --- a/src/app/services/sms/sms.errors.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ApplicationError } from '../../modules/core/core.errors' - -export class SmsSendError extends ApplicationError { - constructor( - message = 'Error sending OTP. Please try again later and if the problem persists, contact us.', - meta?: unknown, - ) { - super(message, meta) - } -} - -export class InvalidNumberError extends ApplicationError { - constructor(message = 'Please enter a valid phone number') { - super(message) - } -} diff --git a/src/app/services/sms/sms.factory.ts b/src/app/services/sms/sms.factory.ts index ab82fd2934..6e299c3079 100644 --- a/src/app/services/sms/sms.factory.ts +++ b/src/app/services/sms/sms.factory.ts @@ -4,13 +4,8 @@ import { useMockTwilio } from '../../config/config' import { ISms, smsConfig } from '../../config/features/sms.config' import { PrismClient } from './sms.dev.prismclient' -import { - sendAdminContactOtp, - sendBouncedSubmissionSms, - sendFormDeactivatedSms, - sendVerificationOtp, -} from './sms.service' -import { BounceNotificationSmsParams, TwilioConfig } from './sms.types' +import { sendVerificationOtp } from './sms.service' +import { TwilioConfig } from './sms.types' interface ISmsFactory { sendVerificationOtp: ( @@ -20,40 +15,6 @@ interface ISmsFactory { formId: string, senderIp: string, ) => ReturnType - sendAdminContactOtp: ( - recipient: string, - otp: string, - userId: string, - senderIp: string, - ) => ReturnType - /** - * Informs recipient that the given form was deactivated. Rejects if SMS feature - * not activated in app. - * @param params Data for SMS to be sent - * @param params.recipient Mobile number to be SMSed - * @param params.recipientEmail The email address of the recipient being SMSed - * @param params.adminId User ID of the admin of the deactivated form - * @param params.adminEmail Email of the admin of the deactivated form - * @param params.formId Form ID of deactivated form - * @param params.formTitle Title of deactivated form - */ - sendFormDeactivatedSms: ( - params: BounceNotificationSmsParams, - ) => ReturnType - /** - * Informs recipient that a response for the given form was lost due to email bounces. - * Rejects if SMS feature not activated in app. - * @param params Data for SMS to be sent - * @param params.recipient Mobile number to be SMSed - * @param params.recipientEmail The email address of the recipient being SMSed - * @param params.adminId User ID of the admin of the form - * @param params.adminEmail Email of the admin of the form - * @param params.formId Form ID of form - * @param params.formTitle Title of form - */ - sendBouncedSubmissionSms: ( - params: BounceNotificationSmsParams, - ) => ReturnType } // Exported for testing. @@ -82,12 +43,6 @@ export const createSmsFactory = (smsConfig: ISms): ISmsFactory => { senderIp, twilioConfig, ), - sendAdminContactOtp: (recipient, otp, userId, senderIp) => - sendAdminContactOtp(recipient, otp, userId, senderIp, twilioConfig), - sendFormDeactivatedSms: (params) => - sendFormDeactivatedSms(params, twilioConfig), - sendBouncedSubmissionSms: (params) => - sendBouncedSubmissionSms(params, twilioConfig), } } diff --git a/src/app/services/sms/sms.service.ts b/src/app/services/sms/sms.service.ts index d85e045e1e..6a6397272b 100644 --- a/src/app/services/sms/sms.service.ts +++ b/src/app/services/sms/sms.service.ts @@ -21,23 +21,19 @@ import { getMongoErrorMessage, transformMongoError, } from '../../utils/handle-mongo-error' +import { + InvalidNumberError, + SmsSendError, +} from '../postman-sms/postman-sms.errors' +import { renderVerificationSms } from '../postman-sms/postman-sms.util' -import { InvalidNumberError, SmsSendError } from './sms.errors' import { - BouncedSubmissionSmsData, - BounceNotificationSmsParams, - FormDeactivatedSmsData, LogSmsParams, LogType, SmsType, TwilioConfig, TwilioCredentials, } from './sms.types' -import { - renderBouncedSubmissionSms, - renderFormDeactivatedSms, - renderVerificationSms, -} from './sms.util' import getSmsCountModel from './sms_count.server.model' const logger = createLoggerWithLabel(module) @@ -362,139 +358,6 @@ export const sendVerificationOtp = ( }) } -export const sendAdminContactOtp = ( - recipient: string, - otp: string, - userId: string, - senderIp: string, - defaultConfig: TwilioConfig, -): ResultAsync => { - logger.info({ - message: `Sending admin contact verification OTP for ${userId}`, - meta: { - action: 'sendAdminContactOtp', - userId, - }, - }) - - const message = `Use the OTP ${otp} to verify your emergency contact number.` - - const otpData: AdminContactOtpData = { - admin: userId, - } - - return sendSms( - defaultConfig, - otpData, - recipient, - message, - SmsType.AdminContact, - senderIp, - ) -} - -/** - * Informs recipient that the given form was deactivated. - * @param params Data for SMS to be sent - * @param params.recipient Mobile number to be SMSed - * @param params.recipientEmail The email address of the recipient being SMSed - * @param params.adminId User ID of the admin of the deactivated form - * @param params.adminEmail Email of the admin of the deactivated form - * @param params.formId Form ID of deactivated form - * @param params.formTitle Title of deactivated form - * @param defaultConfig Twilio configuration - */ -export const sendFormDeactivatedSms = ( - { - recipient, - recipientEmail, - adminId, - adminEmail, - formId, - formTitle, - }: BounceNotificationSmsParams, - defaultConfig: TwilioConfig, -): ResultAsync => { - logger.info({ - message: `Sending form deactivation notification for ${recipientEmail}`, - meta: { - action: 'sendFormDeactivatedSms', - formId, - }, - }) - - const message = renderFormDeactivatedSms(formTitle) - - const smsData: FormDeactivatedSmsData = { - form: formId, - collaboratorEmail: recipientEmail, - recipientNumber: recipient, - formAdmin: { - email: adminEmail, - userId: adminId, - }, - } - - return sendSms( - defaultConfig, - smsData, - recipient, - message, - SmsType.DeactivatedForm, - ) -} - -/** - * Informs recipient that a response for the given form was lost due to email bounces. - * @param params Data for SMS to be sent - * @param params.recipient Mobile number to be SMSed - * @param params.recipientEmail The email address of the recipient being SMSed - * @param params.adminId User ID of the admin of the form - * @param params.adminEmail Email of the admin of the form - * @param params.formId Form ID of form - * @param params.formTitle Title of form - * @param defaultConfig Twilio configuration - */ -export const sendBouncedSubmissionSms = ( - { - recipient, - recipientEmail, - adminId, - adminEmail, - formId, - formTitle, - }: BounceNotificationSmsParams, - defaultConfig: TwilioConfig, -): ResultAsync => { - logger.info({ - message: `Sending bounced submission notification for ${recipientEmail}`, - meta: { - action: 'sendBouncedSubmissionSms', - formId, - }, - }) - - const message = renderBouncedSubmissionSms(formTitle) - - const smsData: BouncedSubmissionSmsData = { - form: formId, - collaboratorEmail: recipientEmail, - recipientNumber: recipient, - formAdmin: { - email: adminEmail, - userId: adminId, - }, - } - - return sendSms( - defaultConfig, - smsData, - recipient, - message, - SmsType.BouncedSubmission, - ) -} - /** * Retrieves the free sms count for a particular user * @param userId The id of the user to retrieve the sms counts for diff --git a/src/app/services/sms/sms.util.ts b/src/app/services/sms/sms.util.ts deleted file mode 100644 index 68f865f9f7..0000000000 --- a/src/app/services/sms/sms.util.ts +++ /dev/null @@ -1,20 +0,0 @@ -import dedent from 'dedent-js' - -export const renderFormDeactivatedSms = (formTitle: string): string => dedent` - Due to responses bouncing from all recipient inboxes, your form "${formTitle}" has been automatically deactivated to prevent further response loss. - - Please ensure your recipient email addresses (Settings tab) have the ability to receive emailed responses from us. Invalid email addresses should be deleted, and full inboxes should be cleared. - - If a systemic email issue is affecting email delivery, consider temporarily deactivating your form until email delivery is stable, or switching the form to Storage mode to continue receiving responses. -` - -export const renderBouncedSubmissionSms = (formTitle: string): string => dedent` - A response to your form "${formTitle}" has bounced from all recipient inboxes. Bounced responses cannot be recovered. To prevent more bounces, please ensure recipient email addresses are correct, and clear any full inboxes. -` - -export const renderVerificationSms = ( - otp: string, - otpPrefix: string, -): string => dedent`Use the OTP ${otpPrefix}-${otp} to submit on FormSG. - - Never share your OTP with anyone else. If you did not request this OTP, you can safely ignore this SMS.` From 0c7086b46cf7e162dff1f3815c2104b7d32925f4 Mon Sep 17 00:00:00 2001 From: Ken Date: Sun, 7 Jul 2024 23:44:21 +0800 Subject: [PATCH 16/16] chore: bump version to v6.133.0 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 724226e13c..8cfa5fd30c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,31 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v6.133.0](https://github.com/opengovsg/FormSG/compare/v6.132.0...v6.133.0) + +- feat(btn): frm 1723 internal flow postman [`#7343`](https://github.com/opengovsg/FormSG/pull/7343) +- chore(deps): bump next and react-email in /react-email-preview [`#7375`](https://github.com/opengovsg/FormSG/pull/7375) +- feat: rename placeholder for logic and mrf components [`#7482`](https://github.com/opengovsg/FormSG/pull/7482) +- chore(deps): remove sentry [`#7480`](https://github.com/opengovsg/FormSG/pull/7480) +- fix(reddot): hide on mrf [`#7481`](https://github.com/opengovsg/FormSG/pull/7481) +- feat(admin-dashboard): add response mode text next to form title on dashboard [`#7462`](https://github.com/opengovsg/FormSG/pull/7462) +- fix(deps): bump jsdom from 21.1.1 to 24.1.0 [`#7345`](https://github.com/opengovsg/FormSG/pull/7345) +- chore(deps-dev): bump @types/jsonwebtoken from 8.5.9 to 9.0.6 [`#7478`](https://github.com/opengovsg/FormSG/pull/7478) +- feat: add reddot on email noti [`#7463`](https://github.com/opengovsg/FormSG/pull/7463) +- chore(privacy policy): update clause 6.3 [`#7477`](https://github.com/opengovsg/FormSG/pull/7477) +- fix(deps): bump uuid and @types/uuid [`#7467`](https://github.com/opengovsg/FormSG/pull/7467) +- feat: add reddot to mrf workflow [`#7428`](https://github.com/opengovsg/FormSG/pull/7428) +- refactor: generalize user seen flag [`#7422`](https://github.com/opengovsg/FormSG/pull/7422) +- feat: add pdf attachments for encrypt forms [`#7470`](https://github.com/opengovsg/FormSG/pull/7470) +- build: merge release v6.132.0 to develop [`#7474`](https://github.com/opengovsg/FormSG/pull/7474) +- build: release v6.132.0 [`#7473`](https://github.com/opengovsg/FormSG/pull/7473) +- add reddot to mrf workflow [`a044b80`](https://github.com/opengovsg/FormSG/commit/a044b805076b6ad14b44fd7baf7c2d73ba73bddc) +- handle old last seen route [`d883952`](https://github.com/opengovsg/FormSG/commit/d883952df327928e92d007c46fae427851a5dc7a) + #### [v6.132.0](https://github.com/opengovsg/FormSG/compare/v6.131.0...v6.132.0) +> 4 July 2024 + - chore: remove nric mask toggle if nricmask is false [`#7472`](https://github.com/opengovsg/FormSG/pull/7472) - fix(deps): bump type-fest from 4.20.1 to 4.21.0 in /shared [`#7468`](https://github.com/opengovsg/FormSG/pull/7468) - fix(deps): bump @babel/runtime from 7.24.1 to 7.24.7 [`#7466`](https://github.com/opengovsg/FormSG/pull/7466) @@ -16,6 +39,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix(encrypt-submission): add missing ndi response fields [`#7425`](https://github.com/opengovsg/FormSG/pull/7425) - build: merge release v6.131.0 to develop [`#7460`](https://github.com/opengovsg/FormSG/pull/7460) - build: release v6.131.0 [`#7459`](https://github.com/opengovsg/FormSG/pull/7459) +- chore: bump version to v6.132.0 [`82da6ac`](https://github.com/opengovsg/FormSG/commit/82da6acb265f9ddfbcaac07f2475c9418b633976) #### [v6.131.0](https://github.com/opengovsg/FormSG/compare/v6.130.1...v6.131.0) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 16cddf316f..704a20cdfc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "form-frontend", - "version": "6.132.0", + "version": "6.133.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "form-frontend", - "version": "6.132.0", + "version": "6.133.0", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^1.8.6", diff --git a/frontend/package.json b/frontend/package.json index 2b9bce4e42..4d7bd36ac1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "form-frontend", - "version": "6.132.0", + "version": "6.133.0", "homepage": ".", "private": true, "dependencies": { diff --git a/package-lock.json b/package-lock.json index 2b303e2d09..ebba7cf609 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "FormSG", - "version": "6.132.0", + "version": "6.133.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "FormSG", - "version": "6.132.0", + "version": "6.133.0", "hasInstallScript": true, "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.536.0", diff --git a/package.json b/package.json index c9ebe57dc9..acd83c3eac 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FormSG", "description": "Form Manager for Government", - "version": "6.132.0", + "version": "6.133.0", "homepage": "https://form.gov.sg", "authors": [ "FormSG "