| @@ -516,6 +516,20 @@ function Header(props) { | |||||
| {isGranted("MAINTAIN_SETTING") ? ( | {isGranted("MAINTAIN_SETTING") ? ( | ||||
| <> | <> | ||||
| <li> | |||||
| <Link className="systemSetting" to="/setting/hkidKeyMigration"> | |||||
| <Typography style={{ opacity: 0.9 }} variant={"pnspsHeaderTitle"} sx={{ ml: 2, mt: 1, mb: 1 }}> | |||||
| HKID Key Migration | |||||
| </Typography> | |||||
| </Link> | |||||
| </li> | |||||
| <li> | |||||
| <Link className="systemSetting" to="/setting/userPiiEncryption"> | |||||
| <Typography style={{ opacity: 0.9 }} variant={"pnspsHeaderTitle"} sx={{ ml: 2, mt: 1, mb: 1 }}> | |||||
| User PII Encryption | |||||
| </Typography> | |||||
| </Link> | |||||
| </li> | |||||
| <li> | <li> | ||||
| <Link className="systemSetting" to="/setting/sys"> | <Link className="systemSetting" to="/setting/sys"> | ||||
| <Typography style={{ opacity: 0.9 }} variant={"pnspsHeaderTitle"} sx={{ ml: 2, mt: 1, mb: 1 }}> | <Typography style={{ opacity: 0.9 }} variant={"pnspsHeaderTitle"} sx={{ ml: 2, mt: 1, mb: 1 }}> | ||||
| @@ -0,0 +1,101 @@ | |||||
| import * as React from "react"; | |||||
| import * as HttpUtils from "utils/HttpUtils"; | |||||
| import * as DateUtils from "utils/DateUtils"; | |||||
| import * as UrlUtils from "utils/ApiPathConst"; | |||||
| import { | |||||
| Grid, Typography, Button, | |||||
| Stack, Box, CircularProgress, | |||||
| } from '@mui/material'; | |||||
| import { notifyActionError } from 'utils/CommonFunction'; | |||||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||||
| const formatLog = (responData) => { | |||||
| if (responData?.msg && !responData?.log) { | |||||
| return <>{DateUtils.datetimeStr(new Date())}<br/><span style={{ color: "red" }}>Error</span><br/>{responData.msg}</>; | |||||
| } | |||||
| const statusColor = responData?.success ? "green" : "red"; | |||||
| const statusText = responData?.success ? "Success" : "Completed with errors"; | |||||
| const logText = (responData?.log || []).join("\n"); | |||||
| return ( | |||||
| <> | |||||
| {DateUtils.datetimeStr(new Date())}<br/> | |||||
| <span style={{ color: statusColor }}>{responData?.dryRun ? "Dry-run " : ""}{statusText}</span><br/> | |||||
| Total: {responData?.total ?? 0}, OK: {responData?.successCount ?? 0}, Skipped: {responData?.skipped ?? 0}, Failed: {responData?.failed ?? 0}<br/> | |||||
| <span style={{ whiteSpace: "pre-line" }}>{logText}</span> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| const HkidKeyMigration = () => { | |||||
| const [resultStr, setResultStr] = React.useState(""); | |||||
| const [wait, setWait] = React.useState(false); | |||||
| const BackgroundHead = { | |||||
| backgroundImage: `url(${titleBackgroundImg})`, | |||||
| width: 'auto', | |||||
| height: 'auto', | |||||
| backgroundSize: 'contain', | |||||
| backgroundRepeat: 'no-repeat', | |||||
| backgroundColor: '#0C489E', | |||||
| backgroundPosition: 'right' | |||||
| }; | |||||
| const runMigration = (dryRun) => { | |||||
| setWait(true); | |||||
| HttpUtils.post({ | |||||
| url: `${UrlUtils.HKID_REKEY_MIGRATION}?dryRun=${dryRun}&batchSize=100`, | |||||
| params: {}, | |||||
| onSuccess: function (responData) { | |||||
| setWait(false); | |||||
| setResultStr(formatLog(responData)); | |||||
| }, | |||||
| onError: function () { | |||||
| setWait(false); | |||||
| notifyActionError("HKID re-key migration failed"); | |||||
| } | |||||
| }); | |||||
| }; | |||||
| return ( | |||||
| <Grid container sx={{ minHeight: '87vh', backgroundColor: "backgroundColor.default" }} direction="column"> | |||||
| <Grid item xs={12}> | |||||
| <div style={BackgroundHead}> | |||||
| <Stack direction="row" height='70px' justifyContent="space-between" alignItems="center"> | |||||
| <Typography ml={15} color='#FFF' variant="h4" sx={{ textShadow: "0px 0px 25px #0C489E" }}> | |||||
| HKID Key Migration | |||||
| </Typography> | |||||
| </Stack> | |||||
| </div> | |||||
| </Grid> | |||||
| <Grid item xs={12} sx={{ backgroundColor: '#ffffff', ml: 2, mt: 1 }} width="98%"> | |||||
| <Box sx={{ borderRadius: '10px', backgroundColor: '#fff', p: 4 }}> | |||||
| <Typography variant="body1" sx={{ mb: 2 }}> | |||||
| Re-encrypt identification and checkDigit from the legacy key to the new key. | |||||
| Ensure Tomcat has both <code>security.hkid-secret</code> and <code>security.hkid-secret-legacy</code> configured before running. | |||||
| </Typography> | |||||
| <Stack direction="row" spacing={2}> | |||||
| <Button variant="outlined" size="large" disabled={wait} onClick={() => runMigration(true)}> | |||||
| Dry Run | |||||
| </Button> | |||||
| <Button variant="contained" size="large" disabled={wait} onClick={() => runMigration(false)}> | |||||
| Run Migration | |||||
| </Button> | |||||
| {wait ? <CircularProgress size={32} /> : null} | |||||
| </Stack> | |||||
| </Box> | |||||
| </Grid> | |||||
| <Grid item xs={12} sx={{ backgroundColor: '#ffffff', ml: 2, mt: 1, mb: 2 }} width="98%"> | |||||
| <Box sx={{ borderRadius: '10px', backgroundColor: '#fff', p: 4 }}> | |||||
| <Typography variant="h4">Result:</Typography> | |||||
| <Box sx={{ pl: 2, pt: 2 }}>{resultStr}</Box> | |||||
| </Box> | |||||
| </Grid> | |||||
| </Grid> | |||||
| ); | |||||
| }; | |||||
| export default HkidKeyMigration; | |||||
| @@ -0,0 +1,133 @@ | |||||
| import * as React from "react"; | |||||
| import * as HttpUtils from "utils/HttpUtils"; | |||||
| import * as DateUtils from "utils/DateUtils"; | |||||
| import * as UrlUtils from "utils/ApiPathConst"; | |||||
| import { | |||||
| Grid, Typography, Button, | |||||
| Stack, Box, CircularProgress, | |||||
| } from '@mui/material'; | |||||
| import { notifyActionError } from 'utils/CommonFunction'; | |||||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||||
| const formatEncryptLog = (responData) => { | |||||
| if (responData?.msg && !responData?.log) { | |||||
| return <>{DateUtils.datetimeStr(new Date())}<br/><span style={{ color: "red" }}>Error</span><br/>{responData.msg}</>; | |||||
| } | |||||
| const statusColor = responData?.success ? "green" : "red"; | |||||
| const statusText = responData?.success ? "Success" : "Completed with errors"; | |||||
| const logText = (responData?.log || []).join("\n"); | |||||
| return ( | |||||
| <> | |||||
| {DateUtils.datetimeStr(new Date())}<br/> | |||||
| <span style={{ color: statusColor }}>{responData?.dryRun ? "Dry-run " : ""}{statusText}</span><br/> | |||||
| Total: {responData?.total ?? 0}, OK: {responData?.successCount ?? 0}, Skipped: {responData?.skipped ?? 0}, Failed: {responData?.failed ?? 0}<br/> | |||||
| <span style={{ whiteSpace: "pre-line" }}>{logText}</span> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| const formatVerifyLog = (responData) => { | |||||
| const tests = responData?.tests || []; | |||||
| const logText = tests.map(t => `${t.passed ? "PASS" : "FAIL"} - ${t.name}: ${t.message}`).join("\n"); | |||||
| const statusColor = responData?.success ? "green" : "red"; | |||||
| return ( | |||||
| <> | |||||
| {DateUtils.datetimeStr(new Date())}<br/> | |||||
| <span style={{ color: statusColor }}>{responData?.success ? "All tests passed" : "Some tests failed"}</span><br/> | |||||
| Passed: {responData?.passed ?? 0}, Failed: {responData?.failed ?? 0}<br/> | |||||
| <span style={{ whiteSpace: "pre-line" }}>{logText}</span> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| const UserPiiEncryption = () => { | |||||
| const [resultStr, setResultStr] = React.useState(""); | |||||
| const [wait, setWait] = React.useState(false); | |||||
| const BackgroundHead = { | |||||
| backgroundImage: `url(${titleBackgroundImg})`, | |||||
| width: 'auto', | |||||
| height: 'auto', | |||||
| backgroundSize: 'contain', | |||||
| backgroundRepeat: 'no-repeat', | |||||
| backgroundColor: '#0C489E', | |||||
| backgroundPosition: 'right' | |||||
| }; | |||||
| const runEncrypt = (dryRun) => { | |||||
| setWait(true); | |||||
| HttpUtils.post({ | |||||
| url: `${UrlUtils.USER_PII_ENCRYPT_MIGRATION}?dryRun=${dryRun}&batchSize=100`, | |||||
| params: {}, | |||||
| onSuccess: function (responData) { | |||||
| setWait(false); | |||||
| setResultStr(formatEncryptLog(responData)); | |||||
| }, | |||||
| onError: function () { | |||||
| setWait(false); | |||||
| notifyActionError("User PII encryption failed"); | |||||
| } | |||||
| }); | |||||
| }; | |||||
| const runVerify = () => { | |||||
| setWait(true); | |||||
| HttpUtils.post({ | |||||
| url: UrlUtils.USER_PII_VERIFY_MIGRATION, | |||||
| params: {}, | |||||
| onSuccess: function (responData) { | |||||
| setWait(false); | |||||
| setResultStr(formatVerifyLog(responData)); | |||||
| }, | |||||
| onError: function () { | |||||
| setWait(false); | |||||
| notifyActionError("User PII verification failed"); | |||||
| } | |||||
| }); | |||||
| }; | |||||
| return ( | |||||
| <Grid container sx={{ minHeight: '87vh', backgroundColor: "backgroundColor.default" }} direction="column"> | |||||
| <Grid item xs={12}> | |||||
| <div style={BackgroundHead}> | |||||
| <Stack direction="row" height='70px' justifyContent="space-between" alignItems="center"> | |||||
| <Typography ml={15} color='#FFF' variant="h4" sx={{ textShadow: "0px 0px 25px #0C489E" }}> | |||||
| User PII Encryption | |||||
| </Typography> | |||||
| </Stack> | |||||
| </div> | |||||
| </Grid> | |||||
| <Grid item xs={12} sx={{ backgroundColor: '#ffffff', ml: 2, mt: 1 }} width="98%"> | |||||
| <Box sx={{ borderRadius: '10px', backgroundColor: '#fff', p: 4 }}> | |||||
| <Typography variant="body1" sx={{ mb: 2 }}> | |||||
| Encrypt enName, chName, mobileNumber, and address for existing user records, then verify data retrieval. | |||||
| </Typography> | |||||
| <Stack direction="row" spacing={2} flexWrap="wrap"> | |||||
| <Button variant="outlined" size="large" disabled={wait} onClick={() => runEncrypt(true)}> | |||||
| Dry Run Encrypt | |||||
| </Button> | |||||
| <Button variant="contained" size="large" disabled={wait} onClick={() => runEncrypt(false)}> | |||||
| Run Encrypt | |||||
| </Button> | |||||
| <Button variant="contained" color="secondary" size="large" disabled={wait} onClick={runVerify}> | |||||
| Verify Retrieval | |||||
| </Button> | |||||
| {wait ? <CircularProgress size={32} /> : null} | |||||
| </Stack> | |||||
| </Box> | |||||
| </Grid> | |||||
| <Grid item xs={12} sx={{ backgroundColor: '#ffffff', ml: 2, mt: 1, mb: 2 }} width="98%"> | |||||
| <Box sx={{ borderRadius: '10px', backgroundColor: '#fff', p: 4 }}> | |||||
| <Typography variant="h4">Result:</Typography> | |||||
| <Box sx={{ pl: 2, pt: 2 }}>{resultStr}</Box> | |||||
| </Box> | |||||
| </Grid> | |||||
| </Grid> | |||||
| ); | |||||
| }; | |||||
| export default UserPiiEncryption; | |||||
| @@ -30,6 +30,8 @@ const EmailTemplateDetailPage = Loadable(lazy(() => import('pages/EmailTemplate/ | |||||
| const HolidayPage = Loadable(lazy(() => import('pages/Holiday'))); | const HolidayPage = Loadable(lazy(() => import('pages/Holiday'))); | ||||
| const GazetteIssuePage = Loadable(lazy(() => import('pages/GazetteIssue/index'))); | const GazetteIssuePage = Loadable(lazy(() => import('pages/GazetteIssue/index'))); | ||||
| const DrImport = Loadable(lazy(() => import('pages/Setting/DrImport'))); | const DrImport = Loadable(lazy(() => import('pages/Setting/DrImport'))); | ||||
| const HkidKeyMigration = Loadable(lazy(() => import('pages/Setting/HkidKeyMigration'))); | |||||
| const UserPiiEncryption = Loadable(lazy(() => import('pages/Setting/UserPiiEncryption'))); | |||||
| const AuditLogPage = Loadable(lazy(() => import('pages/AuditLog/index'))); | const AuditLogPage = Loadable(lazy(() => import('pages/AuditLog/index'))); | ||||
| const ReconReportPage = Loadable(lazy(() => import('pages/Recon'))); | const ReconReportPage = Loadable(lazy(() => import('pages/Recon'))); | ||||
| const ChangePasswordPage = Loadable(lazy(() => import('pages/User/ChangePasswordPage'))); | const ChangePasswordPage = Loadable(lazy(() => import('pages/User/ChangePasswordPage'))); | ||||
| @@ -190,6 +192,18 @@ const GLDUserRoutes = { | |||||
| element: <DrImport /> | element: <DrImport /> | ||||
| }:{}, | }:{}, | ||||
| isGranted("MAINTAIN_SETTING")? | |||||
| { | |||||
| path: '/setting/hkidKeyMigration', | |||||
| element: <HkidKeyMigration /> | |||||
| }:{}, | |||||
| isGranted("MAINTAIN_SETTING")? | |||||
| { | |||||
| path: '/setting/userPiiEncryption', | |||||
| element: <UserPiiEncryption /> | |||||
| }:{}, | |||||
| isGranted("MAINTAIN_SETTING")? | isGranted("MAINTAIN_SETTING")? | ||||
| { | { | ||||
| path: '/setting/auditLog', | path: '/setting/auditLog', | ||||
| @@ -84,6 +84,9 @@ export const GET_FILE_DELETE = apiPath+'/file/delete'; | |||||
| export const DR_EXPORT = apiPath+'/settings/dr/export'; | export const DR_EXPORT = apiPath+'/settings/dr/export'; | ||||
| export const DR_IMPORT = apiPath+'/settings/dr/import'; | export const DR_IMPORT = apiPath+'/settings/dr/import'; | ||||
| export const OFFLINE_IMPORT = apiPath+'/settings/dr/importOffline'; | export const OFFLINE_IMPORT = apiPath+'/settings/dr/importOffline'; | ||||
| export const HKID_REKEY_MIGRATION = apiPath+'/settings/migration/hkid-rekey'; | |||||
| export const USER_PII_ENCRYPT_MIGRATION = apiPath+'/settings/migration/user-pii-encrypt'; | |||||
| export const USER_PII_VERIFY_MIGRATION = apiPath+'/settings/migration/user-pii-verify'; | |||||
| export const AUDIT_LOG_EXPORT = apiPath+'/settings/auditLog-export'; | export const AUDIT_LOG_EXPORT = apiPath+'/settings/auditLog-export'; | ||||