(null);
const { setAuthStore } = useAuth();
const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime);
+ const nextRoute = location.state?.redirectUrl || ROUTES.DRAWER;
const {
register,
@@ -110,7 +111,7 @@ export default function Login() {
// store the auth related data
await setAuthStore(authData.access, userData, masterKey);
- navigate(ROUTES.DRAWER, { replace: true });
+ navigate(nextRoute, { replace: true });
} catch (err) {
let message =
"Sorry, we're experiencing technical issues.\nPlease try again later.";
diff --git a/frontend/src/pages/Reader.test.tsx b/frontend/src/pages/Reader.test.tsx
index 937eddd..8078e1b 100644
--- a/frontend/src/pages/Reader.test.tsx
+++ b/frontend/src/pages/Reader.test.tsx
@@ -1,9 +1,10 @@
import { render, screen, waitFor } from "@testing-library/react";
import { HttpResponse, http } from "msw";
-import { MemoryRouter, Route, Routes } from "react-router-dom";
+import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { server } from "../../test/mocks/server";
import { endpoints } from "../config/endpoints";
+import { useKeyStore } from "../store/useKeyStore";
import { CryptoUtils } from "../utils/crypto";
import Reader from "./Reader";
@@ -19,6 +20,15 @@ describe("Reader Page", () => {
let masterKey: CryptoKey;
let utils: CryptoUtils;
+ const LocationTest = () => {
+ const location = useLocation();
+ return (
+
+ {JSON.stringify(location.state || {})}
+
+ );
+ };
+
beforeEach(async () => {
vi.clearAllMocks();
@@ -26,6 +36,8 @@ describe("Reader Page", () => {
await utils.initialize();
const bundle = await CryptoUtils.deriveKeyBundle("password", "salt");
masterKey = bundle.masterKey;
+ // User is logged in by default
+ useKeyStore.setState({ masterKey });
// Clear the URL hash
vi.stubGlobal("location", {
@@ -34,24 +46,12 @@ describe("Reader Page", () => {
});
});
- it("should notify the user if the sharing key is missing from the URL", async () => {
- render(
-
-
- } />
-
- ,
- );
-
- expect(
- await screen.findByText(/No sharing key provided/i),
- ).toBeInTheDocument();
- });
-
it("should load and decrypt the letter when a valid key is provided and display the envelope", async () => {
const mockPublicId = "test-uuid";
const letterContent = JSON.stringify({ objects: [] });
const metadata = { recipient: "Guest" };
+ // simulate guest
+ useKeyStore.setState({ masterKey: null });
const encryptedLetter = await utils.encryptLetter(letterContent, masterKey);
const encryptedMetadata = await utils.encryptMetadata(metadata, masterKey);
@@ -103,4 +103,39 @@ describe("Reader Page", () => {
await screen.findByText(/Failed to load letter/i),
).toBeInTheDocument();
});
+
+ it("should navigate to the login page with redirect url when the letter has no sharing key and the user is not logged in", async () => {
+ const mockPublicId = "4ef9f25f-4f37-477a-891a-4b10541e350c";
+ const letterContent = JSON.stringify({ objects: [] });
+ const metadata = { recipient: "Guest" };
+ useKeyStore.setState({ masterKey: null });
+
+ const encryptedLetter = await utils.encryptLetter(letterContent, masterKey);
+ const encryptedMetadata = await utils.encryptMetadata(metadata, masterKey);
+
+ server.use(
+ http.get(`${API_URL}${endpoints.LETTERS}${mockPublicId}/`, () => {
+ return HttpResponse.json({
+ encrypted_content: encryptedLetter.encrypted_content,
+ encrypted_metadata: encryptedMetadata.encrypted_content,
+ encrypted_dek: encryptedLetter.encrypted_dek,
+ images: [],
+ });
+ }),
+ );
+
+ render(
+
+
+ } />
+ } />
+
+ ,
+ );
+
+ const stateComponent = screen.getByTestId("location-state");
+ expect(stateComponent).toHaveTextContent(
+ `"redirectUrl":"/read/${mockPublicId}"`,
+ );
+ });
});
diff --git a/frontend/src/pages/Reader.tsx b/frontend/src/pages/Reader.tsx
index 08bb111..acfc5b7 100644
--- a/frontend/src/pages/Reader.tsx
+++ b/frontend/src/pages/Reader.tsx
@@ -6,7 +6,12 @@ import {
XCircleIcon,
} from "@phosphor-icons/react";
import { useEffect, useRef, useState } from "react";
-import { useLocation, useNavigate, useParams } from "react-router-dom";
+import {
+ type NavigateFunction,
+ useLocation,
+ useNavigate,
+ useParams,
+} from "react-router-dom";
import { api } from "../api/apiClient";
import {
type CanvasJSON,
@@ -37,6 +42,7 @@ export default function Reader() {
const navigate = useNavigate();
const sharingKey = location.hash.replace("#", "");
+ const navigateRef = useRef(navigate);
const canvasRef = useRef(null);
const [isDecrypting, setIsDecrypting] = useState(true);
@@ -256,12 +262,9 @@ export default function Reader() {
useEffect(() => {
if (!(sharingKey || masterKey)) {
- setError({
- message:
- "No sharing key provided. Please check the link or log in if you are the author.",
- log: "",
+ navigateRef.current("/login", {
+ state: { redirectUrl: `/read/${public_id}` },
});
- setIsDecrypting(false);
return;
}
@@ -373,7 +376,7 @@ export default function Reader() {
if (isDecrypting) {
return (
-