added detailed reports route

This commit is contained in:
Myzel394 2022-10-23 09:30:46 +02:00
parent 0119c5b409
commit c7381ed5c6
13 changed files with 349 additions and 14 deletions

View File

@ -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",

View File

@ -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: <ReportsRoute />,
},
{
loader: getServerSettings,
path: "/reports/:id",
element: <ReportDetailRoute />,
},
{
path: "/enter-password",
element: <EnterDecryptionPassword />,

13
src/apis/get-report.ts Normal file
View File

@ -0,0 +1,13 @@
import {client} from "~/constants/axios-client"
import {Report} from "~/server-types"
export default async function getReport(id: string): Promise<Report> {
const {data} = await client.get(
`${import.meta.env.VITE_SERVER_BASE_URL}/report/${id}`,
{
withCredentials: true,
},
)
return data
}

View File

@ -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),
}),
),
},
},
}
}

View File

@ -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"

View File

@ -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])

View File

@ -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"

View File

@ -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<boolean>(false)
return (
<>
<ListItemButton
onClick={() => {
if (images.length > 0) {
setShowProxiedImages(value => !value)
}
}}
>
<ListItemIcon>
<BsImage />
</ListItemIcon>
<ListItemText>Proxying {images.length} images</ListItemText>
</ListItemButton>
<Collapse in={showProxiedImages}>
<Box bgcolor={theme.palette.background.default}>
<List>
{images.map(image => (
<ListItemButton
href={image.serverUrl}
target="_blank"
key={image.imageProxyId}
>
<ListItemText
primary={image.url}
secondary={
<>
<Grid
display="flex"
flexDirection="row"
alignItems="center"
container
component="span"
spacing={1}
>
<Grid item component="span">
<MdLocationOn />
</Grid>
<Grid item component="span">
{(() => {
if (
isBefore(
new Date(),
addHours(
image.createdAt,
serverSettings.imageProxyLifeTime,
),
)
) {
return "Stored on Server."
} else {
return "Proxying through Server."
}
})()}
</Grid>
</Grid>
</>
}
/>
</ListItemButton>
))}
</List>
</Box>
</Collapse>
</>
)
}

View File

@ -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<boolean>(false)
const imagesPerTracker = images.reduce((acc, value) => {
acc[value.trackerName] = [...(acc[value.trackerName] || []), value]
return acc
}, {} as Record<string, Array<DecryptedReportContent["messageDetails"]["content"]["singlePixelImages"][0]>>)
return (
<>
<ListItemButton
onClick={() => {
if (images.length > 0) {
setShowImageTrackers(value => !value)
}
}}
>
<ListItemIcon>
<BsShieldShaded />
</ListItemIcon>
<ListItemText>
Removed {images.length} image trackers
</ListItemText>
</ListItemButton>
<Collapse in={showImageTrackers}>
<Box bgcolor={theme.palette.background.default}>
<List>
{Object.entries(imagesPerTracker).map(
([trackerName, images]) => (
<>
<Typography
variant="caption"
component="h3"
ml={1}
>
{trackerName}
</Typography>
{images.map(image => (
<ListItem key={image.source}>
{image.source}
</ListItem>
))}
</>
),
)}
</List>
</Box>
</Collapse>
</>
)
}

View File

@ -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 (
<ListItemButton onClick={() => navigate(`/reports/${report.id}`)}>
<ListItemText
primary={
report.messageDetails.content.subject ?? (
<i>{"<No Subject>"}</i>
)
}
secondary={`${report.messageDetails.meta.from} -> ${report.messageDetails.meta.to}`}
/>
</ListItemButton>
)
}

View File

@ -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<Report, AxiosError>(["get_report", params.id], () =>
getReport(params.id as string),
)
return (
<QueryResult<Report> query={query}>
{encryptedReport => (
<DecryptReport
encryptedContent={encryptedReport.encryptedContent}
>
{report => (
<Grid container spacing={4}>
<Grid item>
<Typography variant="h4" component="h1">
Email Report
</Typography>
</Grid>
<Grid item>
<Typography variant="h6" component="h2">
Email information
</Typography>
<Box component="dl">
<Typography
variant="overline"
component="dt"
>
From
</Typography>
<Typography variant="body1" component="dd">
{report.messageDetails.meta.from}
</Typography>
</Box>
<Box component="dl">
<Typography
variant="overline"
component="dt"
>
To
</Typography>
<Typography variant="body1" component="dd">
{report.messageDetails.meta.to}
</Typography>
</Box>
<Box component="dl">
<Typography
variant="overline"
component="dt"
>
Subject
</Typography>
<Typography variant="body1" component="dd">
{report.messageDetails.content.subject}
</Typography>
</Box>
</Grid>
<Grid item>
<Typography variant="h6" component="h2">
Trackers
</Typography>
<List>
<SinglePixelImageTrackersListItem
images={
report.messageDetails.content
.singlePixelImages
}
/>
<ProxiedImagesListItem
images={
report.messageDetails.content
.proxiedImages
}
/>
</List>
</Grid>
</Grid>
)}
</DecryptReport>
)}
</QueryResult>
)
}

View File

@ -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<PaginationResult<Report>, AxiosError>(
@ -26,17 +27,10 @@ function ReportsRoute(): ReactElement {
encryptedContent={report.encryptedContent}
>
{report => (
<ListItem>
<ListItemText
primary={
report.messageDetails.content
.subject ?? (
<i>{"<No Subject>"}</i>
)
}
secondary={`${report.messageDetails.meta.from} -> ${report.messageDetails.meta.to}`}
></ListItemText>
</ListItem>
<ReportInformationItem
report={report}
key={report.id}
/>
)}
</DecryptReport>
))}

View File

@ -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