diff --git a/src/AuthContext/AuthContext.ts b/src/AuthContext/AuthContext.ts index 61f0201..5355656 100644 --- a/src/AuthContext/AuthContext.ts +++ b/src/AuthContext/AuthContext.ts @@ -10,7 +10,7 @@ interface AuthContextTypeBase { _decryptUsingMasterPassword: (content: string) => string _encryptUsingMasterPassword: (content: string) => string _decryptUsingPrivateKey: (message: string) => Promise - _setDecryptionPassword: (decryptionPassword: string) => void + _setDecryptionPassword: (decryptionPassword: string) => boolean _updateUser: (user: ServerUser | User) => void } diff --git a/src/AuthContext/AuthContextProvider.tsx b/src/AuthContext/AuthContextProvider.tsx index ca1cfae..2c2a699 100644 --- a/src/AuthContext/AuthContextProvider.tsx +++ b/src/AuthContext/AuthContextProvider.tsx @@ -102,6 +102,34 @@ export default function AuthContextProvider({ [user], ) + const updateDecryptionPassword = useCallback( + (password: string): boolean => { + if (!user) { + throw new Error("User not set.") + } + + if (user.isDecrypted) { + // Password already set + return true + } + + try { + // Check if the password is correct + const masterPassword = decryptString( + user.encryptedPassword, + password, + ) + JSON.parse(decryptString(user.encryptedNotes, masterPassword)) + } catch { + return false + } + + setDecryptionPassword(password) + return true + }, + [user?.encryptedPassword], + ) + const value = useMemo( () => ({ user: user ?? null, @@ -111,7 +139,7 @@ export default function AuthContextProvider({ _encryptUsingMasterPassword: encryptUsingMasterPassword, _decryptUsingMasterPassword: decryptUsingMasterPassword, _decryptUsingPrivateKey: decryptUsingPrivateKey, - _setDecryptionPassword: setDecryptionPassword, + _setDecryptionPassword: updateDecryptionPassword, _updateUser: setUser, }), [user, logout, encryptUsingMasterPassword, decryptUsingMasterPassword], diff --git a/src/apis/get-aliases.ts b/src/apis/get-aliases.ts index 3988291..10ada02 100644 --- a/src/apis/get-aliases.ts +++ b/src/apis/get-aliases.ts @@ -1,7 +1,7 @@ -import {Alias} from "~/server-types" +import {Alias, PaginationResult} from "~/server-types" import {client} from "~/constants/axios-client" -export default async function getAliases(): Promise> { +export default async function getAliases(): Promise> { const {data} = await client.get( `${import.meta.env.VITE_SERVER_BASE_URL}/alias`, { diff --git a/src/apis/get-reports.ts b/src/apis/get-reports.ts index 1b66b25..821089d 100644 --- a/src/apis/get-reports.ts +++ b/src/apis/get-reports.ts @@ -1,7 +1,7 @@ -import {Report} from "~/server-types" +import {PaginationResult, Report} from "~/server-types" import {client} from "~/constants/axios-client" -export default async function getReports(): Promise> { +export default async function getReports(): Promise> { const {data} = await client.get( `${import.meta.env.VITE_SERVER_BASE_URL}/report`, { diff --git a/src/apis/helpers/parse-decrypted-report.ts b/src/apis/helpers/parse-decrypted-report.ts new file mode 100644 index 0000000..501cb99 --- /dev/null +++ b/src/apis/helpers/parse-decrypted-report.ts @@ -0,0 +1,16 @@ +import {DecryptedReportContent} from "~/server-types" + +export default function parseDecryptedReport( + report: any, +): DecryptedReportContent { + return { + ...report, + messageDetails: { + ...report.messageDetails, + meta: { + ...report.messageDetails.meta, + createdAt: new Date(report.messageDetails.meta.createdAt), + }, + }, + } +} diff --git a/src/route-widgets/SettingsRoute/DecryptedReport.tsx b/src/route-widgets/SettingsRoute/DecryptReport.tsx similarity index 51% rename from src/route-widgets/SettingsRoute/DecryptedReport.tsx rename to src/route-widgets/SettingsRoute/DecryptReport.tsx index 608dcdd..5afe06f 100644 --- a/src/route-widgets/SettingsRoute/DecryptedReport.tsx +++ b/src/route-widgets/SettingsRoute/DecryptReport.tsx @@ -2,27 +2,31 @@ import {ReactElement, useContext} from "react" import {useAsync} from "react-use" import camelcaseKeys from "camelcase-keys" +import {DecryptedReportContent} from "~/server-types" import AuthContext from "~/AuthContext/AuthContext" +import parseDecryptedReport from "~/apis/helpers/parse-decrypted-report" -export interface DecryptedReportProps { +export interface DecryptReportProps { encryptedContent: string + children: (report: DecryptedReportContent) => ReactElement } -export default function DecryptedReport({ +export default function DecryptReport({ encryptedContent, -}: DecryptedReportProps): ReactElement { + children: render, +}: DecryptReportProps): ReactElement { const {_decryptUsingPrivateKey} = useContext(AuthContext) const {value} = useAsync(async () => { const message = await _decryptUsingPrivateKey(encryptedContent) - return camelcaseKeys(JSON.parse(message)) + const content = camelcaseKeys(JSON.parse(message)) + + return parseDecryptedReport(content) }, [encryptedContent]) if (!value) { return <> } - console.log(value) - - return <> + return render(value) } diff --git a/src/routes/AliasesRoute.tsx b/src/routes/AliasesRoute.tsx index 9f8355c..f61e43a 100644 --- a/src/routes/AliasesRoute.tsx +++ b/src/routes/AliasesRoute.tsx @@ -4,14 +4,14 @@ import {AxiosError} from "axios" import {Grid, List, Typography} from "@mui/material" import {useQuery} from "@tanstack/react-query" -import {Alias} from "~/server-types" +import {Alias, PaginationResult} from "~/server-types" import AliasListItem from "~/route-widgets/AliasRoute/AliasListItem" import CreateRandomAliasButton from "~/route-widgets/AliasRoute/CreateRandomAliasButton" import QueryResult from "~/components/QueryResult" import getAliases from "~/apis/get-aliases" export default function AliasesRoute(): ReactElement { - const query = useQuery, AxiosError>( + const query = useQuery, AxiosError>( ["get_aliases"], getAliases, ) @@ -24,10 +24,10 @@ export default function AliasesRoute(): ReactElement { - > query={query}> - {aliases => ( + > query={query}> + {result => ( - {aliases.map(alias => ( + {result.items.map(alias => ( ))} diff --git a/src/routes/EnterDecryptionPassword.tsx b/src/routes/EnterDecryptionPassword.tsx index ac931f1..7e377a2 100644 --- a/src/routes/EnterDecryptionPassword.tsx +++ b/src/routes/EnterDecryptionPassword.tsx @@ -1,35 +1,83 @@ -import {ReactElement, useContext, useState} from "react" +import * as yup from "yup" +import {ReactElement, useContext} from "react" import {useNavigate} from "react-router-dom" +import {useFormik} from "formik" + import {buildEncryptionPassword} from "~/utils" import {useUser} from "~/hooks" +import {PasswordField, SimpleForm} from "~/components" +import {InputAdornment} from "@mui/material" +import {MdLock} from "react-icons/md" import AuthContext from "~/AuthContext/AuthContext" +interface Form { + password: string +} + +const schema = yup.object().shape({ + password: yup.string().required(), +}) + export default function EnterDecryptionPassword(): ReactElement { const navigate = useNavigate() const user = useUser() const {_setDecryptionPassword} = useContext(AuthContext) - const [password, setPassword] = useState("") + const formik = useFormik
({ + validationSchema: schema, + initialValues: { + password: "", + }, + onSubmit: async ({password}, {setErrors}) => { + const decryptionPassword = buildEncryptionPassword( + password, + user.email.address, + ) + + if (!_setDecryptionPassword(decryptionPassword)) { + setErrors({password: "Password is invalid."}) + } else { + navigate("/") + } + }, + }) return ( -
- setPassword(event.target.value)} - /> - -
+ {[ + + + + ), + }} + />, + ]} + + ) } diff --git a/src/routes/LoginRoute.tsx b/src/routes/LoginRoute.tsx index 1ea3c07..27379ce 100644 --- a/src/routes/LoginRoute.tsx +++ b/src/routes/LoginRoute.tsx @@ -27,7 +27,14 @@ export default function LoginRoute(): ReactElement { key="confirm_code_form" onConfirm={user => { login(user) - navigate("/") + + setTimeout(() => { + if (user.encryptedPassword) { + navigate("/enter-password") + } else { + navigate("/") + } + }, 0) }} email={email} sameRequestToken={sameRequestToken} diff --git a/src/routes/ReportsRoute.tsx b/src/routes/ReportsRoute.tsx index f52b895..3dc9729 100644 --- a/src/routes/ReportsRoute.tsx +++ b/src/routes/ReportsRoute.tsx @@ -1,28 +1,43 @@ -import {useQuery} from "@tanstack/react-query" import {ReactElement} from "react" -import {Report} from "~/server-types" import {AxiosError} from "axios" + +import {useQuery} from "@tanstack/react-query" +import {List, ListItem, ListItemText} from "@mui/material" + +import {PaginationResult, Report} from "~/server-types" import {getReports} from "~/apis" -import DecryptedReport from "~/route-widgets/SettingsRoute/DecryptedReport" +import DecryptReport from "~/route-widgets/SettingsRoute/DecryptReport" import QueryResult from "~/components/QueryResult" export default function ReportsRoute(): ReactElement { - const query = useQuery, AxiosError>( + const query = useQuery, AxiosError>( ["get_reports"], getReports, ) return ( - > query={query}> - {reports => ( - <> - {reports.map(report => ( - > query={query}> + {result => ( + + {result.items.map(report => ( + + > + {report => ( + + ${report.messageDetails.meta.to}`} + > + + )} + ))} - + )} ) diff --git a/src/server-types.ts b/src/server-types.ts index 55748b6..044443e 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -94,6 +94,36 @@ export interface Report { encryptedContent: string } +export interface DecryptedReportContent { + version: "1.0" + messageDetails: { + meta: { + from: string + to: string + createdAt: Date + } + content: { + subject: string + proxiedImages: Array<{ + url: string + imageProxyId: string + }> + singlePixelImages: Array<{ + source: string + trackerName: string + trackerUrl: string + }> + } + } +} + +export type PaginationResult = { + items: T[] + total: number + page: number + size: number +} + export interface UserNote { theme: Theme privateKey: string