mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 07:55:25 +02:00
added detailed reports route
This commit is contained in:
parent
0119c5b409
commit
c7381ed5c6
@ -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",
|
||||
|
@ -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
13
src/apis/get-report.ts
Normal 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
|
||||
}
|
@ -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),
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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])
|
@ -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"
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
29
src/route-widgets/ReportsRoute/ReportInformationItem.tsx
Normal file
29
src/route-widgets/ReportsRoute/ReportInformationItem.tsx
Normal 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>
|
||||
)
|
||||
}
|
98
src/routes/ReportDetailRoute.tsx
Normal file
98
src/routes/ReportDetailRoute.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
))}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user