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",
|
"crypto-js": "^4.1.1",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
|
"group-array": "^1.0.0",
|
||||||
"in-seconds": "^1.2.0",
|
"in-seconds": "^1.2.0",
|
||||||
"openpgp": "^5.5.0",
|
"openpgp": "^5.5.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@ -37,6 +38,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/date-fns": "^2.6.0",
|
"@types/date-fns": "^2.6.0",
|
||||||
|
"@types/group-array": "^1.0.1",
|
||||||
"@types/openpgp": "^4.4.18",
|
"@types/openpgp": "^4.4.18",
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.0.17",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
|
@ -14,6 +14,7 @@ import AuthenticatedRoute from "~/routes/AuthenticatedRoute"
|
|||||||
import CompleteAccountRoute from "~/routes/CompleteAccountRoute"
|
import CompleteAccountRoute from "~/routes/CompleteAccountRoute"
|
||||||
import EnterDecryptionPassword from "~/routes/EnterDecryptionPassword"
|
import EnterDecryptionPassword from "~/routes/EnterDecryptionPassword"
|
||||||
import LoginRoute from "~/routes/LoginRoute"
|
import LoginRoute from "~/routes/LoginRoute"
|
||||||
|
import ReportDetailRoute from "~/routes/ReportDetailRoute"
|
||||||
import ReportsRoute from "~/routes/ReportsRoute"
|
import ReportsRoute from "~/routes/ReportsRoute"
|
||||||
import RootRoute from "~/routes/Root"
|
import RootRoute from "~/routes/Root"
|
||||||
import SettingsRoute from "~/routes/SettingsRoute"
|
import SettingsRoute from "~/routes/SettingsRoute"
|
||||||
@ -67,6 +68,11 @@ const router = createBrowserRouter([
|
|||||||
path: "/reports",
|
path: "/reports",
|
||||||
element: <ReportsRoute />,
|
element: <ReportsRoute />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
loader: getServerSettings,
|
||||||
|
path: "/reports/:id",
|
||||||
|
element: <ReportDetailRoute />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/enter-password",
|
path: "/enter-password",
|
||||||
element: <EnterDecryptionPassword />,
|
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,
|
...report.messageDetails.meta,
|
||||||
createdAt: new Date(report.messageDetails.meta.createdAt),
|
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 {default as updatePreferences} from "./update-preferences"
|
||||||
export * from "./get-reports"
|
export * from "./get-reports"
|
||||||
export {default as getReports} 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 {value} = useAsync(async () => {
|
||||||
const message = await _decryptUsingPrivateKey(encryptedContent)
|
const message = await _decryptUsingPrivateKey(encryptedContent)
|
||||||
const content = camelcaseKeys(JSON.parse(message))
|
const content = camelcaseKeys(JSON.parse(message), {deep: true})
|
||||||
|
|
||||||
return parseDecryptedReport(content)
|
return parseDecryptedReport(content)
|
||||||
}, [encryptedContent])
|
}, [encryptedContent])
|
@ -18,3 +18,5 @@ export * from "./SuccessSnack"
|
|||||||
export {default as SuccessSnack} from "./SuccessSnack"
|
export {default as SuccessSnack} from "./SuccessSnack"
|
||||||
export * from "./ErrorLoadingDataMessage"
|
export * from "./ErrorLoadingDataMessage"
|
||||||
export {default as ErrorLoadingDataMessage} 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 {AxiosError} from "axios"
|
||||||
|
|
||||||
import {useQuery} from "@tanstack/react-query"
|
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 {PaginationResult, Report} from "~/server-types"
|
||||||
import {getReports} from "~/apis"
|
import {getReports} from "~/apis"
|
||||||
import {WithEncryptionRequired} from "~/hocs"
|
import {WithEncryptionRequired} from "~/hocs"
|
||||||
import DecryptReport from "~/route-widgets/SettingsRoute/DecryptReport"
|
import {DecryptReport} from "~/components"
|
||||||
import QueryResult from "~/components/QueryResult"
|
import QueryResult from "~/components/QueryResult"
|
||||||
|
import ReportInformationItem from "~/route-widgets/ReportsRoute/ReportInformationItem"
|
||||||
|
|
||||||
function ReportsRoute(): ReactElement {
|
function ReportsRoute(): ReactElement {
|
||||||
const query = useQuery<PaginationResult<Report>, AxiosError>(
|
const query = useQuery<PaginationResult<Report>, AxiosError>(
|
||||||
@ -26,17 +27,10 @@ function ReportsRoute(): ReactElement {
|
|||||||
encryptedContent={report.encryptedContent}
|
encryptedContent={report.encryptedContent}
|
||||||
>
|
>
|
||||||
{report => (
|
{report => (
|
||||||
<ListItem>
|
<ReportInformationItem
|
||||||
<ListItemText
|
report={report}
|
||||||
primary={
|
key={report.id}
|
||||||
report.messageDetails.content
|
/>
|
||||||
.subject ?? (
|
|
||||||
<i>{"<No Subject>"}</i>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
secondary={`${report.messageDetails.meta.from} -> ${report.messageDetails.meta.to}`}
|
|
||||||
></ListItemText>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
)}
|
||||||
</DecryptReport>
|
</DecryptReport>
|
||||||
))}
|
))}
|
||||||
|
@ -96,8 +96,10 @@ export interface Report {
|
|||||||
|
|
||||||
export interface DecryptedReportContent {
|
export interface DecryptedReportContent {
|
||||||
version: "1.0"
|
version: "1.0"
|
||||||
|
id: string
|
||||||
messageDetails: {
|
messageDetails: {
|
||||||
meta: {
|
meta: {
|
||||||
|
messageId: string
|
||||||
from: string
|
from: string
|
||||||
to: string
|
to: string
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
@ -107,6 +109,8 @@ export interface DecryptedReportContent {
|
|||||||
proxiedImages: Array<{
|
proxiedImages: Array<{
|
||||||
url: string
|
url: string
|
||||||
imageProxyId: string
|
imageProxyId: string
|
||||||
|
createdAt: Date
|
||||||
|
serverUrl: string
|
||||||
}>
|
}>
|
||||||
singlePixelImages: Array<{
|
singlePixelImages: Array<{
|
||||||
source: string
|
source: string
|
||||||
|
Loading…
x
Reference in New Issue
Block a user