cracked the approach for live form editing; password change (#822)

This commit is contained in:
Michael Quigley 2025-01-27 13:48:29 -05:00
parent e3cd4f9254
commit f962a7d253
No known key found for this signature in database
GPG Key ID: 9B60314A9DD20A62
3 changed files with 107 additions and 9 deletions

View File

@ -20,6 +20,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router": "^7.0.1",
"yup": "^1.6.1",
"zustand": "^5.0.2"
},
"devDependencies": {
@ -4118,6 +4119,12 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/property-expr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -4464,6 +4471,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
"license": "MIT"
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
@ -4483,6 +4496,12 @@
"node": ">=8.0"
}
},
"node_modules/toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
"license": "MIT"
},
"node_modules/ts-api-utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz",
@ -4521,6 +4540,18 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
@ -4605,9 +4636,9 @@
}
},
"node_modules/vite": {
"version": "5.4.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
"version": "5.4.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4719,6 +4750,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yup": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz",
"integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==",
"license": "MIT",
"dependencies": {
"property-expr": "^2.0.5",
"tiny-case": "^1.0.3",
"toposort": "^2.0.2",
"type-fest": "^2.19.0"
}
},
"node_modules/zustand": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.2.tgz",

View File

@ -22,6 +22,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router": "^7.0.1",
"yup": "^1.6.1",
"zustand": "^5.0.2"
},
"devDependencies": {

View File

@ -1,8 +1,10 @@
import {User} from "./model/user.ts";
import {useState} from "react";
import {useEffect, useState} from "react";
import {modalStyle} from "./styling/theme.ts";
import {Box, Button, Grid2, Modal, TextField, Typography} from "@mui/material";
import {useFormik} from "formik";
import * as Yup from 'yup';
import {getAccountApi} from "./model/api.ts";
interface AccountPasswordChangeModalProps {
close: () => void;
@ -12,6 +14,8 @@ interface AccountPasswordChangeModalProps {
const AccountPasswordChangeModal =({ close, isOpen, user }: AccountPasswordChangeModalProps) => {
const [errorMessage, setErrorMessage] = useState<React.JSX.Element>(null);
const submitButton = <Button color="primary" variant="contained" type="submit" sx={{ mt: 2 }}>Change Password</Button>;
const [bottomControl, setBottomControl] = useState<React.JSX.Element>(submitButton);
const passwordChangeForm = useFormik({
initialValues: {
@ -20,11 +24,54 @@ const AccountPasswordChangeModal =({ close, isOpen, user }: AccountPasswordChang
duplicateNewPassword: "",
},
onSubmit: v => {
setErrorMessage(null as React.JSX.Element);
// api
}
setErrorMessage(null);
getAccountApi(user).changePassword({
body: {
email: user.email,
oldPassword: v.currentPassword,
newPassword: v.newPassword,
}
})
.then(d => {
setBottomControl(<Typography>Your password has been changed!</Typography>);
})
.catch(e => {
setErrorMessage(<Typography color="red">Password change failed! Check your current password!</Typography>);
})
},
validationSchema: Yup.object({
currentPassword: Yup.string()
.required("Current password is required"),
newPassword: Yup.string()
.min(8, "Password must be at least 8 characters")
.max(64, "Password must be less than 64 characters")
.matches(
/^.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?].*$/,
"Password requires at least one special character"
)
.matches(
/^.*[a-z].*$/,
"Password requires at least one lowercase letter"
)
.matches(
/^.*[A-Z].*$/,
"Password requires at least one uppercase letter"
)
.required("Password is required"),
duplicateNewPassword: Yup.string()
.required("Please confirm your new password")
.test("password-matches", "Password confirmation does not match", v => v === passwordChangeForm.values.newPassword)
}),
});
useEffect(() => {
passwordChangeForm.values.currentPassword = "";
passwordChangeForm.values.newPassword = "";
passwordChangeForm.values.duplicateNewPassword = "";
setErrorMessage(null);
setBottomControl(submitButton);
}, [isOpen]);
return (
<Modal open={isOpen} onClose={close}>
<Box sx={{ ...modalStyle }}>
@ -42,6 +89,8 @@ const AccountPasswordChangeModal =({ close, isOpen, user }: AccountPasswordChang
value={passwordChangeForm.values.currentPassword}
onChange={passwordChangeForm.handleChange}
onBlur={passwordChangeForm.handleBlur}
error={passwordChangeForm.errors.currentPassword !== undefined}
helperText={passwordChangeForm.errors.currentPassword}
sx={{ mt: 2 }}
/>
<TextField
@ -53,6 +102,8 @@ const AccountPasswordChangeModal =({ close, isOpen, user }: AccountPasswordChang
value={passwordChangeForm.values.newPassword}
onChange={passwordChangeForm.handleChange}
onBlur={passwordChangeForm.handleBlur}
error={passwordChangeForm.errors.newPassword !== undefined}
helperText={passwordChangeForm.errors.newPassword}
sx={{ mt: 2 }}
/>
<TextField
@ -60,15 +111,18 @@ const AccountPasswordChangeModal =({ close, isOpen, user }: AccountPasswordChang
type="password"
id="duplicateNewPassword"
name="duplicateNewPassword"
label="Re-enter New Password"
label="Confirm New Password"
value={passwordChangeForm.values.duplicateNewPassword}
onChange={passwordChangeForm.handleChange}
onBlur={passwordChangeForm.handleBlur}
error={passwordChangeForm.errors.duplicateNewPassword !== undefined}
helperText={passwordChangeForm.errors.duplicateNewPassword}
sx={{ mt: 2 }}
/>
</Grid2>
{ errorMessage ? <Grid2 container sx={{ mt: 2, p: 1}}><Typography>{errorMessage}</Typography></Grid2> : null}
<Grid2 container sx={{ flexGrow: 1, p: 1 }} alignItems="center">
<Button color="primary" variant="contained" type="submit" sx={{ mt: 2 }}>Change Password</Button>
{bottomControl}
</Grid2>
</form>
</Box>