mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 08:56:52 +00:00
feat: implement dynamic redirection after login based on location state
This commit is contained in:
@@ -61,7 +61,7 @@ export function EnvelopeReveal({
|
|||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
className={
|
className={
|
||||||
"translate-y-24 delay-2000 absolute z-6 peer-has-checked:pointer-events-none peer-has-checked:opacity-0 peer-has-checked:delay-0 transition-opacity duration-1500 cursor-pointer"
|
"translate-y-24 delay-2000 absolute z-6 peer-has-checked:pointer-events-none peer-has-checked:opacity-0 peer-has-checked:delay-0 transition-opacity duration-1000 cursor-pointer"
|
||||||
}
|
}
|
||||||
src={waxSeal}
|
src={waxSeal}
|
||||||
alt="Seal"
|
alt="Seal"
|
||||||
@@ -71,7 +71,7 @@ export function EnvelopeReveal({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
id="letter"
|
id="letter"
|
||||||
className={`absolute mx-auto transition-all peer-has-checked:delay-800 peer-has-checked:duration-1000 duration-1000 mt-2 h-55 w-105 bg-paper peer-has-checked:-mt-12 hover:-mt-24 cursor-pointer ${revealLetter ? "duration-1000 peer-has-checked:duration-2000 w-screen max-w-4xl h-screen z-101 -translate-y-90" : "peer-has-checked:z-1"}`}
|
className={`absolute mx-auto transition-all peer-has-checked:delay-800 peer-has-checked:duration-1000 duration-1000 mt-2 h-55 w-105 bg-paper peer-has-checked:-mt-12 hover:-mt-24 cursor-pointer ${revealLetter ? "duration-1000 peer-has-checked:duration-3000 w-screen max-w-4xl h-screen z-101 -translate-y-90" : "peer-has-checked:z-1"}`}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
></button>
|
></button>
|
||||||
|
|
||||||
@@ -91,8 +91,9 @@ export function EnvelopeReveal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
id="env-front"
|
||||||
type="button"
|
type="button"
|
||||||
className="p-10 absolute inset-0 backface-hidden w-110 bg-base-200 z-99 rounded-md -translate-x-2"
|
className="text-left p-10 absolute inset-0 backface-hidden w-110 bg-base-200 z-99 rounded-md -translate-x-2"
|
||||||
onClick={() => setIsFlipped((prev) => !prev)}
|
onClick={() => setIsFlipped((prev) => !prev)}
|
||||||
>
|
>
|
||||||
<span className={"text-neutral-content/60 font-xs font-display"}>
|
<span className={"text-neutral-content/60 font-xs font-display"}>
|
||||||
|
|||||||
@@ -44,7 +44,19 @@ describe("Login Page", () => {
|
|||||||
expect(await screen.findByText(/technical issues/i)).toBeInTheDocument();
|
expect(await screen.findByText(/technical issues/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should redirect to the drawer when login is successful", async () => {
|
it.each([
|
||||||
|
{
|
||||||
|
locationState: undefined,
|
||||||
|
nextRoute: "Drawer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
locationState: { redirectUrl: "/read/123" },
|
||||||
|
nextRoute: "Reader",
|
||||||
|
},
|
||||||
|
])("should redirect to the next route when login is successful", async ({
|
||||||
|
locationState,
|
||||||
|
nextRoute,
|
||||||
|
}) => {
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
public_id: "user-123",
|
public_id: "user-123",
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
@@ -61,10 +73,18 @@ describe("Login Page", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={["/login"]}>
|
<MemoryRouter
|
||||||
|
initialEntries={[
|
||||||
|
{
|
||||||
|
pathname: "/login",
|
||||||
|
state: locationState,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/drawer" element={<div>Drawer</div>} />
|
<Route path="/drawer" element={<div>Drawer</div>} />
|
||||||
|
<Route path="/read/:publicId" element={<div>Reader</div>} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
@@ -73,6 +93,6 @@ describe("Login Page", () => {
|
|||||||
await userEvent.type(screen.getByLabelText(/password/i), "password123");
|
await userEvent.type(screen.getByLabelText(/password/i), "password123");
|
||||||
await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
|
await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
|
||||||
|
|
||||||
expect(await screen.findByText(/Drawer/i)).toBeInTheDocument();
|
expect(await screen.findByText(nextRoute)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export default function Login() {
|
|||||||
const [apiError, setApiError] = useState<string | null>(null);
|
const [apiError, setApiError] = useState<string | null>(null);
|
||||||
const { setAuthStore } = useAuth();
|
const { setAuthStore } = useAuth();
|
||||||
const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime);
|
const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime);
|
||||||
|
const nextRoute = location.state?.redirectUrl || ROUTES.DRAWER;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -110,7 +111,7 @@ export default function Login() {
|
|||||||
// store the auth related data
|
// store the auth related data
|
||||||
await setAuthStore(authData.access, userData, masterKey);
|
await setAuthStore(authData.access, userData, masterKey);
|
||||||
|
|
||||||
navigate(ROUTES.DRAWER, { replace: true });
|
navigate(nextRoute, { replace: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let message =
|
let message =
|
||||||
"Sorry, we're experiencing technical issues.\nPlease try again later.";
|
"Sorry, we're experiencing technical issues.\nPlease try again later.";
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
import { HttpResponse, http } from "msw";
|
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 { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { server } from "../../test/mocks/server";
|
import { server } from "../../test/mocks/server";
|
||||||
import { endpoints } from "../config/endpoints";
|
import { endpoints } from "../config/endpoints";
|
||||||
|
import { useKeyStore } from "../store/useKeyStore";
|
||||||
import { CryptoUtils } from "../utils/crypto";
|
import { CryptoUtils } from "../utils/crypto";
|
||||||
import Reader from "./Reader";
|
import Reader from "./Reader";
|
||||||
|
|
||||||
@@ -19,6 +20,15 @@ describe("Reader Page", () => {
|
|||||||
let masterKey: CryptoKey;
|
let masterKey: CryptoKey;
|
||||||
let utils: CryptoUtils;
|
let utils: CryptoUtils;
|
||||||
|
|
||||||
|
const LocationTest = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
return (
|
||||||
|
<div data-testid="location-state">
|
||||||
|
{JSON.stringify(location.state || {})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
@@ -26,6 +36,8 @@ describe("Reader Page", () => {
|
|||||||
await utils.initialize();
|
await utils.initialize();
|
||||||
const bundle = await CryptoUtils.deriveKeyBundle("password", "salt");
|
const bundle = await CryptoUtils.deriveKeyBundle("password", "salt");
|
||||||
masterKey = bundle.masterKey;
|
masterKey = bundle.masterKey;
|
||||||
|
// User is logged in by default
|
||||||
|
useKeyStore.setState({ masterKey });
|
||||||
|
|
||||||
// Clear the URL hash
|
// Clear the URL hash
|
||||||
vi.stubGlobal("location", {
|
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(
|
|
||||||
<MemoryRouter initialEntries={["/read/123"]}>
|
|
||||||
<Routes>
|
|
||||||
<Route path="/read/:public_id" element={<Reader />} />
|
|
||||||
</Routes>
|
|
||||||
</MemoryRouter>,
|
|
||||||
);
|
|
||||||
|
|
||||||
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 () => {
|
it("should load and decrypt the letter when a valid key is provided and display the envelope", async () => {
|
||||||
const mockPublicId = "test-uuid";
|
const mockPublicId = "test-uuid";
|
||||||
const letterContent = JSON.stringify({ objects: [] });
|
const letterContent = JSON.stringify({ objects: [] });
|
||||||
const metadata = { recipient: "Guest" };
|
const metadata = { recipient: "Guest" };
|
||||||
|
// simulate guest
|
||||||
|
useKeyStore.setState({ masterKey: null });
|
||||||
|
|
||||||
const encryptedLetter = await utils.encryptLetter(letterContent, masterKey);
|
const encryptedLetter = await utils.encryptLetter(letterContent, masterKey);
|
||||||
const encryptedMetadata = await utils.encryptMetadata(metadata, masterKey);
|
const encryptedMetadata = await utils.encryptMetadata(metadata, masterKey);
|
||||||
@@ -103,4 +103,39 @@ describe("Reader Page", () => {
|
|||||||
await screen.findByText(/Failed to load letter/i),
|
await screen.findByText(/Failed to load letter/i),
|
||||||
).toBeInTheDocument();
|
).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(
|
||||||
|
<MemoryRouter initialEntries={[`/read/${mockPublicId}`]}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/read/:public_id" element={<Reader />} />
|
||||||
|
<Route path="/login" element={<LocationTest />} />
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const stateComponent = screen.getByTestId("location-state");
|
||||||
|
expect(stateComponent).toHaveTextContent(
|
||||||
|
`"redirectUrl":"/read/${mockPublicId}"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import {
|
|||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { useEffect, useRef, useState } from "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 { api } from "../api/apiClient";
|
||||||
import {
|
import {
|
||||||
type CanvasJSON,
|
type CanvasJSON,
|
||||||
@@ -37,6 +42,7 @@ export default function Reader() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const sharingKey = location.hash.replace("#", "");
|
const sharingKey = location.hash.replace("#", "");
|
||||||
|
|
||||||
|
const navigateRef = useRef<NavigateFunction>(navigate);
|
||||||
const canvasRef = useRef<CanvasTools>(null);
|
const canvasRef = useRef<CanvasTools>(null);
|
||||||
|
|
||||||
const [isDecrypting, setIsDecrypting] = useState(true);
|
const [isDecrypting, setIsDecrypting] = useState(true);
|
||||||
@@ -256,12 +262,9 @@ export default function Reader() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!(sharingKey || masterKey)) {
|
if (!(sharingKey || masterKey)) {
|
||||||
setError({
|
navigateRef.current("/login", {
|
||||||
message:
|
state: { redirectUrl: `/read/${public_id}` },
|
||||||
"No sharing key provided. Please check the link or log in if you are the author.",
|
|
||||||
log: "",
|
|
||||||
});
|
});
|
||||||
setIsDecrypting(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +376,7 @@ export default function Reader() {
|
|||||||
|
|
||||||
if (isDecrypting) {
|
if (isDecrypting) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-base-100 font-serif">
|
<div className="flex items-center justify-center bg-base-100 font-serif">
|
||||||
<div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.4)_100%)] pointer-events-none z-0" />
|
<div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.4)_100%)] pointer-events-none z-0" />
|
||||||
<div className="text-center space-y-6 z-10">
|
<div className="text-center space-y-6 z-10">
|
||||||
<Logo />
|
<Logo />
|
||||||
|
|||||||
Reference in New Issue
Block a user