diff options
Diffstat (limited to 'packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx')
-rw-r--r-- | packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx new file mode 100644 index 000000000..2724fba11 --- /dev/null +++ b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx @@ -0,0 +1,319 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + AbsoluteTime, + HttpStatusCode, + TalerErrorCode, + TranslatedString, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + LocalNotificationBanner, + ShowInputErrorLabel, + notifyInfo, + useLocalNotification, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; +import { useSessionState } from "../../hooks/session.js"; +import { useBankState } from "../../hooks/bank-state.js"; +import { RouteDefinition } from "@gnu-taler/web-util/browser"; +import { undefinedIfEmpty } from "../../utils.js"; +import { doAutoFocus } from "../PaytoWireTransferForm.js"; +import { ProfileNavigation } from "../ProfileNavigation.js"; + +export function UpdateAccountPassword({ + account: accountName, + routeClose, + onUpdateSuccess, + onAuthorizationRequired, + routeMyAccountCashout, + routeMyAccountDelete, + routeMyAccountDetails, + routeMyAccountPassword, + routeConversionConfig, + focus, + routeHere, +}: { + routeClose: RouteDefinition; + routeHere: RouteDefinition<{ account: string }>; + routeMyAccountDetails: RouteDefinition; + routeMyAccountDelete: RouteDefinition; + routeMyAccountPassword: RouteDefinition; + routeMyAccountCashout: RouteDefinition; + routeConversionConfig: RouteDefinition; + focus?: boolean; + onAuthorizationRequired: () => void; + onUpdateSuccess: () => void; + account: string; +}): VNode { + const { i18n } = useTranslationContext(); + const { state: credentials } = useSessionState(); + const token = + credentials.status !== "loggedIn" ? undefined : credentials.token; + const { + lib: { bank: api }, + } = useBankCoreApiContext(); + + const [current, setCurrent] = useState<string | undefined>(); + const [password, setPassword] = useState<string | undefined>(); + const [repeat, setRepeat] = useState<string | undefined>(); + const [, updateBankState] = useBankState(); + + const accountIsTheCurrentUser = + credentials.status === "loggedIn" + ? credentials.username === accountName + : false; + + const errors = undefinedIfEmpty({ + current: !accountIsTheCurrentUser + ? undefined + : !current + ? i18n.str`Required` + : undefined, + password: !password ? i18n.str`Required` : undefined, + repeat: !repeat + ? i18n.str`Required` + : password !== repeat + ? i18n.str`Repeated password doesn't match` + : undefined, + }); + const [notification, notify, handleError] = useLocalNotification(); + + async function doChangePassword() { + if (!!errors || !password || !token) return; + await handleError(async () => { + const request = { + old_password: current, + new_password: password, + }; + const resp = await api.updatePassword( + { username: accountName, token }, + request, + ); + if (resp.type === "ok") { + notifyInfo(i18n.str`Password changed`); + onUpdateSuccess(); + } else { + switch (resp.case) { + case HttpStatusCode.Unauthorized: + return notify({ + type: "error", + title: i18n.str`Not authorized to change the password, maybe the session is invalid.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`Account not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + case TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD: + return notify({ + type: "error", + title: i18n.str`You need to provide the old password. If you don't have it contact your account administrator.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + case TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD: + return notify({ + type: "error", + title: i18n.str`Your current password doesn't match, can't change to a new password.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + case HttpStatusCode.Accepted: { + updateBankState("currentChallenge", { + operation: "update-password", + id: String(resp.body.challenge_id), + location: routeHere.url({ account: accountName }), + sent: AbsoluteTime.never(), + request, + }); + return onAuthorizationRequired(); + } + default: + assertUnreachable(resp); + } + } + }); + } + + return ( + <Fragment> + <LocalNotificationBanner notification={notification} /> + {accountIsTheCurrentUser ? ( + <ProfileNavigation + current="credentials" + routeMyAccountCashout={routeMyAccountCashout} + routeMyAccountDelete={routeMyAccountDelete} + routeMyAccountDetails={routeMyAccountDetails} + routeMyAccountPassword={routeMyAccountPassword} + routeConversionConfig={routeConversionConfig} + /> + ) : ( + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Account "{accountName}"</i18n.Translate> + </h1> + )} + + <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> + <div class="px-4 sm:px-0"> + <h2 class="text-base font-semibold leading-7 text-gray-900"> + <i18n.Translate>Update password</i18n.Translate> + </h2> + </div> + <form + class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" + autoCapitalize="none" + autoCorrect="off" + onSubmit={(e) => { + e.preventDefault(); + }} + > + <div class="px-4 py-6 sm:p-8"> + <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + {accountIsTheCurrentUser ? ( + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="password" + > + {i18n.str`Current password`} + <b style={{ color: "red" }}> *</b> + </label> + <div class="mt-2"> + <input + type="password" + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + name="current" + id="current-password" + data-error={!!errors?.current && current !== undefined} + value={current ?? ""} + onChange={(e) => { + setCurrent(e.currentTarget.value); + }} + autocomplete="off" + /> + <ShowInputErrorLabel + message={errors?.current} + isDirty={current !== undefined} + /> + </div> + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate> + Your current password, for security + </i18n.Translate> + </p> + </div> + ) : undefined} + + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="password" + > + {i18n.str`New password`} + <b style={{ color: "red" }}> *</b> + </label> + <div class="mt-2"> + <input + ref={focus ? doAutoFocus : undefined} + type="password" + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + name="password" + id="password" + data-error={!!errors?.password && password !== undefined} + value={password ?? ""} + onChange={(e) => { + setPassword(e.currentTarget.value); + }} + autocomplete="off" + /> + <ShowInputErrorLabel + message={errors?.password} + isDirty={password !== undefined} + /> + </div> + </div> + + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="repeat" + > + {i18n.str`Type it again`} + <b style={{ color: "red" }}> *</b> + </label> + <div class="mt-2"> + <input + type="password" + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + name="repeat" + id="repeat" + data-error={!!errors?.repeat && repeat !== undefined} + value={repeat ?? ""} + onChange={(e) => { + setRepeat(e.currentTarget.value); + }} + // placeholder="" + autocomplete="off" + /> + <ShowInputErrorLabel + message={errors?.repeat} + isDirty={repeat !== undefined} + /> + </div> + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate>Repeat the same password</i18n.Translate> + </p> + </div> + </div> + </div> + <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> + <a + href={routeClose.url({})} + name="cancel" + class="text-sm font-semibold leading-6 text-gray-900" + > + <i18n.Translate>Cancel</i18n.Translate> + </a> + <button + type="submit" + name="change" + class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + disabled={!!errors} + onClick={(e) => { + e.preventDefault(); + doChangePassword(); + }} + > + <i18n.Translate>Change</i18n.Translate> + </button> + </div> + </form> + </div> + </Fragment> + ); +} |