diff --git a/package.json b/package.json index f2ffac5..7215662 100755 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "crypto-js": "^4.1.1", "date-fns": "^2.29.3", "formik": "^2.2.9", + "group-array": "^1.0.0", "in-seconds": "^1.2.0", "openpgp": "^5.5.0", "react": "^18.2.0", @@ -37,6 +38,7 @@ "devDependencies": { "@types/crypto-js": "^4.1.1", "@types/date-fns": "^2.6.0", + "@types/group-array": "^1.0.1", "@types/openpgp": "^4.4.18", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", diff --git a/src/App.tsx b/src/App.tsx index a407e1f..0483b09 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,7 @@ import AuthenticatedRoute from "~/routes/AuthenticatedRoute" import CompleteAccountRoute from "~/routes/CompleteAccountRoute" import EnterDecryptionPassword from "~/routes/EnterDecryptionPassword" import LoginRoute from "~/routes/LoginRoute" +import ReportDetailRoute from "~/routes/ReportDetailRoute" import ReportsRoute from "~/routes/ReportsRoute" import RootRoute from "~/routes/Root" import SettingsRoute from "~/routes/SettingsRoute" @@ -67,6 +68,11 @@ const router = createBrowserRouter([ path: "/reports", element: , }, + { + loader: getServerSettings, + path: "/reports/:id", + element: , + }, { path: "/enter-password", element: , diff --git a/src/apis/get-report.ts b/src/apis/get-report.ts new file mode 100644 index 0000000..9112c3b --- /dev/null +++ b/src/apis/get-report.ts @@ -0,0 +1,13 @@ +import {client} from "~/constants/axios-client" +import {Report} from "~/server-types" + +export default async function getReport(id: string): Promise { + const {data} = await client.get( + `${import.meta.env.VITE_SERVER_BASE_URL}/report/${id}`, + { + withCredentials: true, + }, + ) + + return data +} diff --git a/src/apis/helpers/parse-decrypted-report.ts b/src/apis/helpers/parse-decrypted-report.ts index 501cb99..3c1abb5 100644 --- a/src/apis/helpers/parse-decrypted-report.ts +++ b/src/apis/helpers/parse-decrypted-report.ts @@ -11,6 +11,15 @@ export default function parseDecryptedReport( ...report.messageDetails.meta, createdAt: new Date(report.messageDetails.meta.createdAt), }, + content: { + ...report.messageDetails.content, + proxiedImages: report.messageDetails.content.proxiedImages.map( + image => ({ + ...image, + createdAt: new Date(image.createdAt), + }), + ), + }, }, } } diff --git a/src/apis/index.ts b/src/apis/index.ts index 1f94589..140337f 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -28,3 +28,5 @@ export * from "./update-preferences" export {default as updatePreferences} from "./update-preferences" export * from "./get-reports" export {default as getReports} from "./get-reports" +export * from "./get-report" +export {default as getReport} from "./get-report" diff --git a/src/route-widgets/SettingsRoute/DecryptReport.tsx b/src/components/DecryptReport.tsx similarity index 92% rename from src/route-widgets/SettingsRoute/DecryptReport.tsx rename to src/components/DecryptReport.tsx index 5afe06f..7980d97 100644 --- a/src/route-widgets/SettingsRoute/DecryptReport.tsx +++ b/src/components/DecryptReport.tsx @@ -19,7 +19,7 @@ export default function DecryptReport({ const {value} = useAsync(async () => { const message = await _decryptUsingPrivateKey(encryptedContent) - const content = camelcaseKeys(JSON.parse(message)) + const content = camelcaseKeys(JSON.parse(message), {deep: true}) return parseDecryptedReport(content) }, [encryptedContent]) diff --git a/src/components/index.ts b/src/components/index.ts index 351baf2..e10ea74 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -18,3 +18,5 @@ export * from "./SuccessSnack" export {default as SuccessSnack} from "./SuccessSnack" export * from "./ErrorLoadingDataMessage" export {default as ErrorLoadingDataMessage} from "./ErrorLoadingDataMessage" +export * from "./DecryptReport" +export {default as DecryptReport} from "./DecryptReport" diff --git a/src/route-widgets/ReportDetailRoute/ProxiedImagesListItem.tsx b/src/route-widgets/ReportDetailRoute/ProxiedImagesListItem.tsx new file mode 100644 index 0000000..399e6b1 --- /dev/null +++ b/src/route-widgets/ReportDetailRoute/ProxiedImagesListItem.tsx @@ -0,0 +1,99 @@ +import {BsImage} from "react-icons/bs" +import {ReactElement, useState} from "react" +import {MdLocationOn} from "react-icons/md" +import {useLoaderData} from "react-router-dom" +import addHours from "date-fns/addHours" +import isBefore from "date-fns/isBefore" + +import { + Box, + Collapse, + Grid, + List, + ListItemButton, + ListItemIcon, + ListItemText, + useTheme, +} from "@mui/material" + +import {DecryptedReportContent, ServerSettings} from "~/server-types" + +export interface ProxiedImagesListItemProps { + images: DecryptedReportContent["messageDetails"]["content"]["proxiedImages"] +} + +export default function ProxiedImagesListItem({ + images, +}: ProxiedImagesListItemProps): ReactElement { + const serverSettings = useLoaderData() as ServerSettings + const theme = useTheme() + + const [showProxiedImages, setShowProxiedImages] = useState(false) + + return ( + <> + { + if (images.length > 0) { + setShowProxiedImages(value => !value) + } + }} + > + + + + Proxying {images.length} images + + + + + {images.map(image => ( + + + + + + + + {(() => { + if ( + isBefore( + new Date(), + addHours( + image.createdAt, + serverSettings.imageProxyLifeTime, + ), + ) + ) { + return "Stored on Server." + } else { + return "Proxying through Server." + } + })()} + + + + } + /> + + ))} + + + + + ) +} diff --git a/src/route-widgets/ReportDetailRoute/SinglePixelImageTrackersListItem.tsx b/src/route-widgets/ReportDetailRoute/SinglePixelImageTrackersListItem.tsx new file mode 100644 index 0000000..c94b04d --- /dev/null +++ b/src/route-widgets/ReportDetailRoute/SinglePixelImageTrackersListItem.tsx @@ -0,0 +1,77 @@ +import {ReactElement, useState} from "react" + +import { + Box, + Collapse, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Typography, + useTheme, +} from "@mui/material" + +import {DecryptedReportContent} from "~/server-types" +import {BsShieldShaded} from "react-icons/bs" + +export interface SinglePixelImageTrackersListItemProps { + images: DecryptedReportContent["messageDetails"]["content"]["singlePixelImages"] +} + +export default function SinglePixelImageTrackersListItem({ + images, +}: SinglePixelImageTrackersListItemProps): ReactElement { + const theme = useTheme() + + const [showImageTrackers, setShowImageTrackers] = useState(false) + + const imagesPerTracker = images.reduce((acc, value) => { + acc[value.trackerName] = [...(acc[value.trackerName] || []), value] + + return acc + }, {} as Record>) + + return ( + <> + { + if (images.length > 0) { + setShowImageTrackers(value => !value) + } + }} + > + + + + + Removed {images.length} image trackers + + + + + + {Object.entries(imagesPerTracker).map( + ([trackerName, images]) => ( + <> + + {trackerName} + + {images.map(image => ( + + {image.source} + + ))} + + ), + )} + + + + + ) +} diff --git a/src/route-widgets/ReportsRoute/ReportInformationItem.tsx b/src/route-widgets/ReportsRoute/ReportInformationItem.tsx new file mode 100644 index 0000000..ca563f5 --- /dev/null +++ b/src/route-widgets/ReportsRoute/ReportInformationItem.tsx @@ -0,0 +1,29 @@ +import {ReactElement} from "react" +import {useNavigate} from "react-router-dom" + +import {ListItemButton, ListItemText} from "@mui/material" + +import {DecryptedReportContent} from "~/server-types" + +export interface ReportInformationItemProps { + report: DecryptedReportContent +} + +export default function ReportInformationItem({ + report, +}: ReportInformationItemProps): ReactElement { + const navigate = useNavigate() + + return ( + navigate(`/reports/${report.id}`)}> + {""} + ) + } + secondary={`${report.messageDetails.meta.from} -> ${report.messageDetails.meta.to}`} + /> + + ) +} diff --git a/src/routes/ReportDetailRoute.tsx b/src/routes/ReportDetailRoute.tsx new file mode 100644 index 0000000..0d8a5c4 --- /dev/null +++ b/src/routes/ReportDetailRoute.tsx @@ -0,0 +1,98 @@ +import {useParams} from "react-router-dom" +import {AxiosError} from "axios" +import React, {ReactElement} from "react" + +import {useQuery} from "@tanstack/react-query" +import {Box, Grid, List, Typography} from "@mui/material" + +import {Report} from "~/server-types" +import {getReport} from "~/apis" +import {DecryptReport} from "~/components" +import ProxiedImagesListItem from "~/route-widgets/ReportDetailRoute/ProxiedImagesListItem" +import QueryResult from "~/components/QueryResult" +import SinglePixelImageTrackersListItem from "~/route-widgets/ReportDetailRoute/SinglePixelImageTrackersListItem" + +export default function ReportDetailRoute(): ReactElement { + const params = useParams() + + const query = useQuery(["get_report", params.id], () => + getReport(params.id as string), + ) + + return ( + query={query}> + {encryptedReport => ( + + {report => ( + + + + Email Report + + + + + Email information + + + + From + + + {report.messageDetails.meta.from} + + + + + To + + + {report.messageDetails.meta.to} + + + + + Subject + + + {report.messageDetails.content.subject} + + + + + + Trackers + + + + + + + + )} + + )} + + ) +} diff --git a/src/routes/ReportsRoute.tsx b/src/routes/ReportsRoute.tsx index 8211793..ddd57e9 100644 --- a/src/routes/ReportsRoute.tsx +++ b/src/routes/ReportsRoute.tsx @@ -2,13 +2,14 @@ import {ReactElement} from "react" import {AxiosError} from "axios" import {useQuery} from "@tanstack/react-query" -import {List, ListItem, ListItemText} from "@mui/material" +import {List} from "@mui/material" import {PaginationResult, Report} from "~/server-types" import {getReports} from "~/apis" import {WithEncryptionRequired} from "~/hocs" -import DecryptReport from "~/route-widgets/SettingsRoute/DecryptReport" +import {DecryptReport} from "~/components" import QueryResult from "~/components/QueryResult" +import ReportInformationItem from "~/route-widgets/ReportsRoute/ReportInformationItem" function ReportsRoute(): ReactElement { const query = useQuery, AxiosError>( @@ -26,17 +27,10 @@ function ReportsRoute(): ReactElement { encryptedContent={report.encryptedContent} > {report => ( - - {""} - ) - } - secondary={`${report.messageDetails.meta.from} -> ${report.messageDetails.meta.to}`} - > - + )} ))} diff --git a/src/server-types.ts b/src/server-types.ts index d475675..61c31e9 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -96,8 +96,10 @@ export interface Report { export interface DecryptedReportContent { version: "1.0" + id: string messageDetails: { meta: { + messageId: string from: string to: string createdAt: Date @@ -107,6 +109,8 @@ export interface DecryptedReportContent { proxiedImages: Array<{ url: string imageProxyId: string + createdAt: Date + serverUrl: string }> singlePixelImages: Array<{ source: string