From 01a0b40d3961864232e16569604795821bd9d08b Mon Sep 17 00:00:00 2001 From: Ian <52504170+ibacher@users.noreply.github.com> Date: Tue, 2 Jul 2024 14:01:33 -0400 Subject: [PATCH] (feat) O3-3423: Allow user to change password (#1047) Co-authored-by: gitcliff <46714226+gitcliff@users.noreply.github.com> --- package.json | 7 +- .../change-location-link.test.tsx | 2 +- .../change-password-link.extension.tsx | 25 +++ .../change-password-link.test.tsx | 24 +++ .../change-password-modal.scss | 9 ++ .../change-password/change-password.modal.tsx | 142 ++++++++++++++++++ .../change-password.resource.ts | 12 ++ .../src/change-password/change-password.scss | 15 ++ packages/apps/esm-login-app/src/index.ts | 9 +- packages/apps/esm-login-app/src/routes.json | 13 ++ .../apps/esm-login-app/translations/en.json | 12 ++ packages/framework/esm-framework/docs/API.md | 32 ++-- packages/framework/esm-styleguide/mock.tsx | 1 + .../src/icons/icon-registration.ts | 2 + .../esm-styleguide/src/icons/icons.tsx | 6 + .../src/icons/svgs/password.svg | 4 + yarn.lock | 28 ++++ 17 files changed, 327 insertions(+), 16 deletions(-) create mode 100644 packages/apps/esm-login-app/src/change-password/change-password-link.extension.tsx create mode 100644 packages/apps/esm-login-app/src/change-password/change-password-link.test.tsx create mode 100644 packages/apps/esm-login-app/src/change-password/change-password-modal.scss create mode 100644 packages/apps/esm-login-app/src/change-password/change-password.modal.tsx create mode 100644 packages/apps/esm-login-app/src/change-password/change-password.resource.ts create mode 100644 packages/apps/esm-login-app/src/change-password/change-password.scss create mode 100644 packages/framework/esm-styleguide/src/icons/svgs/password.svg diff --git a/package.json b/package.json index 2cfcba055..ed5efca2d 100644 --- a/package.json +++ b/package.json @@ -76,5 +76,10 @@ "@carbon/icons-react": "11.37.0", "minipass": "3.3.5" }, - "packageManager": "yarn@4.2.2" + "packageManager": "yarn@4.2.2", + "dependencies": { + "@hookform/resolvers": "^3.6.0", + "react-hook-form": "^7.52.0", + "zod": "^3.23.8" + } } diff --git a/packages/apps/esm-login-app/src/change-location-link/change-location-link.test.tsx b/packages/apps/esm-login-app/src/change-location-link/change-location-link.test.tsx index 09b866e16..45a196c49 100644 --- a/packages/apps/esm-login-app/src/change-location-link/change-location-link.test.tsx +++ b/packages/apps/esm-login-app/src/change-location-link/change-location-link.test.tsx @@ -8,7 +8,7 @@ const navigateMock = navigate as jest.Mock; const useSessionMock = useSession as jest.Mock; delete window.location; -window.location = new URL('https://dev3.openmrs.org/openmrs/spa/home') as any as Location; +window.location = new URL('https://dev3.openmrs.org/openmrs/spa/home') as unknown as Location; describe('', () => { beforeEach(() => { diff --git a/packages/apps/esm-login-app/src/change-password/change-password-link.extension.tsx b/packages/apps/esm-login-app/src/change-password/change-password-link.extension.tsx new file mode 100644 index 000000000..c3c1b484e --- /dev/null +++ b/packages/apps/esm-login-app/src/change-password/change-password-link.extension.tsx @@ -0,0 +1,25 @@ +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, SwitcherItem } from '@carbon/react'; +import { PasswordIcon, showModal } from '@openmrs/esm-framework'; +import styles from './change-password.scss'; + +const ChangePasswordLink: React.FC = () => { + const { t } = useTranslation(); + + const launchChangePasswordModal = useCallback(() => showModal('change-password-modal'), []); + + return ( + + + + {t('password', 'Password')} + + + {t('change', 'Change')} + + + ); +}; + +export default ChangePasswordLink; diff --git a/packages/apps/esm-login-app/src/change-password/change-password-link.test.tsx b/packages/apps/esm-login-app/src/change-password/change-password-link.test.tsx new file mode 100644 index 000000000..9bb0f8cd4 --- /dev/null +++ b/packages/apps/esm-login-app/src/change-password/change-password-link.test.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { render, screen } from '@testing-library/react'; +import { showModal } from '@openmrs/esm-framework'; +import ChangePasswordLink from './change-password-link.extension'; + +const mockShowModal = jest.mocked(showModal); + +describe('', () => { + it('should display the `Change password` link', async () => { + const user = userEvent.setup(); + + render(); + + const changePasswordLink = screen.getByRole('button', { + name: /Change/i, + }); + + await user.click(changePasswordLink); + + expect(mockShowModal).toHaveBeenCalledTimes(1); + expect(mockShowModal).toHaveBeenCalledWith('change-password-modal'); + }); +}); diff --git a/packages/apps/esm-login-app/src/change-password/change-password-modal.scss b/packages/apps/esm-login-app/src/change-password/change-password-modal.scss new file mode 100644 index 000000000..f68cabf32 --- /dev/null +++ b/packages/apps/esm-login-app/src/change-password/change-password-modal.scss @@ -0,0 +1,9 @@ +.submitButton { + :global(.cds--inline-loading) { + min-height: 1rem; + } + + :global(.cds--inline-loading__text) { + font-size: unset; + } +} diff --git a/packages/apps/esm-login-app/src/change-password/change-password.modal.tsx b/packages/apps/esm-login-app/src/change-password/change-password.modal.tsx new file mode 100644 index 000000000..9be5c56bc --- /dev/null +++ b/packages/apps/esm-login-app/src/change-password/change-password.modal.tsx @@ -0,0 +1,142 @@ +import React, { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Controller, FormProvider, useForm, type SubmitHandler } from 'react-hook-form'; +import { Button, Form, PasswordInput, InlineLoading, ModalBody, ModalFooter, ModalHeader, Stack } from '@carbon/react'; +import { showSnackbar } from '@openmrs/esm-framework'; +import { changeUserPassword } from './change-password.resource'; +import styles from './change-password-modal.scss'; + +interface ChangePasswordModalProps { + close(): () => void; +} + +const ChangePasswordModal: React.FC = ({ close }) => { + const { t } = useTranslation(); + const [isChangingPassword, setIsChangingPassword] = useState(false); + + const oldPasswordValidation = z.string({ + required_error: t('oldPasswordRequired', 'Old password is required'), + }); + + const newPasswordValidation = z.string({ + required_error: t('newPasswordRequired', 'New password is required'), + }); + + const passwordConfirmationValidation = z.string({ + required_error: t('passwordConfirmationRequired', 'Password confirmation is required'), + }); + + const changePasswordFormSchema = z + .object({ + oldPassword: oldPasswordValidation, + newPassword: newPasswordValidation, + passwordConfirmation: passwordConfirmationValidation, + }) + .refine((data) => data.newPassword === data.passwordConfirmation, { + message: t('passwordsDoNotMatch', 'Passwords do not match'), + path: ['passwordConfirmation'], + }); + + const methods = useForm>({ + mode: 'all', + resolver: zodResolver(changePasswordFormSchema), + }); + + const onSubmit: SubmitHandler> = useCallback((data) => { + setIsChangingPassword(true); + + const { oldPassword, newPassword } = data; + + changeUserPassword(oldPassword, newPassword) + .then(() => { + close(); + + showSnackbar({ + title: t('passwordChangedSuccessfully', 'Password changed successfully'), + kind: 'success', + }); + }) + .catch((error) => { + showSnackbar({ + kind: 'error', + subtitle: error?.message, + title: t('errorChangingPassword', 'Error changing password'), + }); + }) + .finally(() => { + setIsChangingPassword(false); + }); + }, []); + + const onError = () => setIsChangingPassword(false); + + return ( + + + + + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + + + + {t('cancel', 'Cancel')} + + + {isChangingPassword ? ( + + ) : ( + {t('change', 'Change')} + )} + + + + + ); +}; + +export default ChangePasswordModal; diff --git a/packages/apps/esm-login-app/src/change-password/change-password.resource.ts b/packages/apps/esm-login-app/src/change-password/change-password.resource.ts new file mode 100644 index 000000000..f99cb7ed2 --- /dev/null +++ b/packages/apps/esm-login-app/src/change-password/change-password.resource.ts @@ -0,0 +1,12 @@ +import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; + +export function changeUserPassword(oldPassword: string, newPassword: string) { + return openmrsFetch(`${restBaseUrl}/password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: { + oldPassword, + newPassword, + }, + }); +} diff --git a/packages/apps/esm-login-app/src/change-password/change-password.scss b/packages/apps/esm-login-app/src/change-password/change-password.scss new file mode 100644 index 000000000..41a9c6468 --- /dev/null +++ b/packages/apps/esm-login-app/src/change-password/change-password.scss @@ -0,0 +1,15 @@ +.alignCenter { + display: flex; + text-align: center; +} + +.panelItemContainer { + a { + @extend .alignCenter; + justify-content: space-between; + } + + div { + @extend .alignCenter; + } +} diff --git a/packages/apps/esm-login-app/src/index.ts b/packages/apps/esm-login-app/src/index.ts index 711c58d03..9db8a0c4c 100644 --- a/packages/apps/esm-login-app/src/index.ts +++ b/packages/apps/esm-login-app/src/index.ts @@ -1,9 +1,10 @@ -import { defineConfigSchema, getSyncLifecycle } from '@openmrs/esm-framework'; +import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle } from '@openmrs/esm-framework'; import { configSchema } from './config-schema'; -import rootComponent from './root.component'; -import locationPickerComponent from './location-picker/location-picker.component'; import changeLocationLinkComponent from './change-location-link/change-location-link.extension'; +import changePasswordLinkComponent from './change-password/change-password-link.extension'; +import locationPickerComponent from './location-picker/location-picker.component'; import logoutButtonComponent from './logout/logout.extension'; +import rootComponent from './root.component'; const moduleName = '@openmrs/esm-login-app'; @@ -22,3 +23,5 @@ export const root = getSyncLifecycle(rootComponent, options); export const locationPicker = getSyncLifecycle(locationPickerComponent, options); export const logoutButton = getSyncLifecycle(logoutButtonComponent, options); export const changeLocationLink = getSyncLifecycle(changeLocationLinkComponent, options); +export const changePasswordLink = getSyncLifecycle(changePasswordLinkComponent, options); +export const changePasswordModal = getAsyncLifecycle(() => import('./change-password/change-password.modal'), options); diff --git a/packages/apps/esm-login-app/src/routes.json b/packages/apps/esm-login-app/src/routes.json index adc91ef0f..6d615e774 100644 --- a/packages/apps/esm-login-app/src/routes.json +++ b/packages/apps/esm-login-app/src/routes.json @@ -32,6 +32,13 @@ "online": true, "offline": true }, + { + "name": "password-changer", + "slot": "user-panel-slot", + "component": "changePasswordLink", + "online": true, + "offline": true + }, { "name": "location-changer", "slot": "user-panel-slot", @@ -40,5 +47,11 @@ "offline": true, "order": 1 } + ], + "modals": [ + { + "name": "change-password-modal", + "component": "changePasswordModal" + } ] } diff --git a/packages/apps/esm-login-app/translations/en.json b/packages/apps/esm-login-app/translations/en.json index 9026532d8..54182cdba 100644 --- a/packages/apps/esm-login-app/translations/en.json +++ b/packages/apps/esm-login-app/translations/en.json @@ -1,8 +1,13 @@ { "back": "Back", "backToUserNameIconLabel": "Back to username", + "cancel": "Cancel", "change": "Change", + "changePassword": "Change password", + "changingLanguage": "Changing password", + "confirmPassword": "Confirm new password", "continue": "Continue", + "errorChangingPassword": "Error changing password", "errorLoadingLoginLocations": "Error loading login locations", "invalidCredentials": "Invalid username or password", "loading": "Loading", @@ -15,9 +20,16 @@ "loggingIn": "Logging in", "login": "Log in", "Logout": "Logout", + "newPassword": "New password", + "newPasswordRequired": "New password is required", "noResultsToDisplay": "No results to display", + "oldPassword": "Old password", + "oldPasswordRequired": "Old password is required", "openmrsLogo": "OpenMRS logo", "password": "Password", + "passwordChangedSuccessfully": "Password changed successfully", + "passwordConfirmationRequired": "Password confirmation is required", + "passwordsDoNotMatch": "Passwords do not match", "poweredBy": "Powered by", "rememberLocationForFutureLogins": "Remember my location for future logins", "searchForLocation": "Search for a location", diff --git a/packages/framework/esm-framework/docs/API.md b/packages/framework/esm-framework/docs/API.md index e0ebdf428..8ebe2ed45 100644 --- a/packages/framework/esm-framework/docs/API.md +++ b/packages/framework/esm-framework/docs/API.md @@ -679,7 +679,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:577](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L577) +[packages/framework/esm-styleguide/src/icons/icons.tsx:583](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L583) ___ @@ -1131,7 +1131,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:551](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L551) +[packages/framework/esm-styleguide/src/icons/icons.tsx:557](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L557) ___ @@ -1181,7 +1181,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:556](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L556) +[packages/framework/esm-styleguide/src/icons/icons.tsx:562](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L562) ___ @@ -1345,7 +1345,7 @@ Note this is an alias for ListCheckedIcon #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:563](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L563) +[packages/framework/esm-styleguide/src/icons/icons.tsx:569](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L569) ___ @@ -1457,7 +1457,7 @@ This is a utility type for custom icons that use the svg-sprite-loader to bundle #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:585](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L585) +[packages/framework/esm-styleguide/src/icons/icons.tsx:591](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L591) ___ @@ -1595,6 +1595,16 @@ ___ ___ +### PasswordIcon + +• `Const` **PasswordIcon**: `MemoExoticComponent`<`ForwardRefExoticComponent`<[`IconProps`](API.md#iconprops) & `RefAttributes`<`SVGSVGElement`\>\>\> + +#### Defined in + +[packages/framework/esm-styleguide/src/icons/icons.tsx:514](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L514) + +___ + ### PedestrianFamilyIcon • `Const` **PedestrianFamilyIcon**: `MemoExoticComponent`<`ForwardRefExoticComponent`<[`IconProps`](API.md#iconprops) & `RefAttributes`<`SVGSVGElement`\>\>\> @@ -1643,7 +1653,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:568](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L568) +[packages/framework/esm-styleguide/src/icons/icons.tsx:574](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L574) ___ @@ -1697,7 +1707,7 @@ Note this is an alias for ShoppingCartArrowDownIcon #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:575](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L575) +[packages/framework/esm-styleguide/src/icons/icons.tsx:581](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L581) ___ @@ -1837,7 +1847,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:516](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L516) +[packages/framework/esm-styleguide/src/icons/icons.tsx:522](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L522) ___ @@ -1857,7 +1867,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:532](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L532) +[packages/framework/esm-styleguide/src/icons/icons.tsx:538](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L538) ___ @@ -1867,7 +1877,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:524](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L524) +[packages/framework/esm-styleguide/src/icons/icons.tsx:530](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L530) ___ @@ -1877,7 +1887,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:540](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L540) +[packages/framework/esm-styleguide/src/icons/icons.tsx:546](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L546) ___ diff --git a/packages/framework/esm-styleguide/mock.tsx b/packages/framework/esm-styleguide/mock.tsx index 16308d857..7b8f13278 100644 --- a/packages/framework/esm-styleguide/mock.tsx +++ b/packages/framework/esm-styleguide/mock.tsx @@ -41,6 +41,7 @@ export const PenIcon = () => PenIcon; export const PrinterIcon = () => PrinterIcon; export const RenewIcon = () => RenewIcon; export const ResetIcon = () => ResetIcon; +export const PasswordIcon = () => PasswordIcon; export const SaveIcon = () => SaveIcon; export const SearchIcon = () => SearchIcon; export const SwitcherIcon = () => SwitcherIcon; diff --git a/packages/framework/esm-styleguide/src/icons/icon-registration.ts b/packages/framework/esm-styleguide/src/icons/icon-registration.ts index 050569b71..61a82478b 100644 --- a/packages/framework/esm-styleguide/src/icons/icon-registration.ts +++ b/packages/framework/esm-styleguide/src/icons/icon-registration.ts @@ -45,6 +45,7 @@ import pedestrianFamily from './svgs/pedestrian-family.svg'; import pen from './svgs/pen.svg'; import printer from './svgs/printer.svg'; import renew from './svgs/renew.svg'; +import password from './svgs/password.svg'; import reset from './svgs/reset.svg'; import save from './svgs/save.svg'; import search from './svgs/search.svg'; @@ -114,6 +115,7 @@ export function setupIcons() { addSvg('omrs-icon-pen', pen); addSvg('omrs-icon-printer', printer); addSvg('omrs-icon-renew', renew); + addSvg('omrs-icon-password', password); addSvg('omrs-icon-reset', reset); addSvg('omrs-icon-search', search); addSvg('omrs-icon-save', save); diff --git a/packages/framework/esm-styleguide/src/icons/icons.tsx b/packages/framework/esm-styleguide/src/icons/icons.tsx index 800f05dad..d44948307 100644 --- a/packages/framework/esm-styleguide/src/icons/icons.tsx +++ b/packages/framework/esm-styleguide/src/icons/icons.tsx @@ -511,6 +511,12 @@ export const UserXrayIcon = memo( }), ); +export const PasswordIcon = memo( + forwardRef(function PasswordIcon(props, ref) { + return ; + }), +); + /** */ export const UserIcon = memo( diff --git a/packages/framework/esm-styleguide/src/icons/svgs/password.svg b/packages/framework/esm-styleguide/src/icons/svgs/password.svg new file mode 100644 index 000000000..6bbc044f6 --- /dev/null +++ b/packages/framework/esm-styleguide/src/icons/svgs/password.svg @@ -0,0 +1,4 @@ + + + + diff --git a/yarn.lock b/yarn.lock index ef9da9863..5ebf96a79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1910,6 +1910,15 @@ __metadata: languageName: node linkType: hard +"@hookform/resolvers@npm:^3.6.0": + version: 3.6.0 + resolution: "@hookform/resolvers@npm:3.6.0" + peerDependencies: + react-hook-form: ^7.0.0 + checksum: 10/6dd1b7ad21ed2b171470740884e0b83982c79a0d4ceddabe60b616e53eeed2b5569cdba5e91ad844e379aeda5ffa835b0c2d2525d702fcd8b263c2194895f9b7 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.13": version: 0.11.13 resolution: "@humanwhocodes/config-array@npm:0.11.13" @@ -3003,6 +3012,7 @@ __metadata: version: 0.0.0-use.local resolution: "@openmrs/esm-core@workspace:." dependencies: + "@hookform/resolvers": "npm:^3.6.0" "@playwright/test": "npm:1.44.0" "@swc/core": "npm:^1.3.58" "@swc/jest": "npm:^0.2.29" @@ -3034,6 +3044,7 @@ __metadata: openmrs: "workspace:*" postcss: "npm:^8.4.6" prettier: "npm:^3.1.0" + react-hook-form: "npm:^7.52.0" swc-loader: "npm:^0.2.3" systemjs-webpack-interop: "npm:^2.3.7" timezone-mock: "npm:^1.2.2" @@ -3044,6 +3055,7 @@ __metadata: typedoc-plugin-no-inherit: "npm:^1.3.1" typescript: "npm:~4.6.4" webpack: "npm:^5.88.0" + zod: "npm:^3.23.8" languageName: unknown linkType: soft @@ -15861,6 +15873,15 @@ __metadata: languageName: node linkType: hard +"react-hook-form@npm:^7.52.0": + version: 7.52.0 + resolution: "react-hook-form@npm:7.52.0" + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + checksum: 10/fc4f92008acc22bcdabf7f472529b29dd29f6305f2b66c8e993c72cbc43eb03b761dd55d5dcb339382fe4bfdd81c4521d3ddcc380d43a3c9a8501aec121e4e7d + languageName: node + linkType: hard + "react-i18next@npm:^11.18.6": version: 11.18.6 resolution: "react-i18next@npm:11.18.6" @@ -19541,6 +19562,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:^3.23.8": + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 10/846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1 + languageName: node + linkType: hard + "zustand@npm:^4.3.6": version: 4.3.6 resolution: "zustand@npm:4.3.6"
{t('password', 'Password')}