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>
|
||||
<img
|
||||
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}
|
||||
alt="Seal"
|
||||
@@ -71,7 +71,7 @@ export function EnvelopeReveal({
|
||||
<button
|
||||
type="button"
|
||||
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}
|
||||
></button>
|
||||
|
||||
@@ -91,8 +91,9 @@ export function EnvelopeReveal({
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="env-front"
|
||||
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)}
|
||||
>
|
||||
<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();
|
||||
});
|
||||
|
||||
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 = {
|
||||
public_id: "user-123",
|
||||
email: "test@example.com",
|
||||
@@ -61,10 +73,18 @@ describe("Login Page", () => {
|
||||
);
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/login"]}>
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
{
|
||||
pathname: "/login",
|
||||
state: locationState,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/drawer" element={<div>Drawer</div>} />
|
||||
<Route path="/read/:publicId" element={<div>Reader</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
@@ -73,6 +93,6 @@ describe("Login Page", () => {
|
||||
await userEvent.type(screen.getByLabelText(/password/i), "password123");
|
||||
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 { 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.";
|
||||
|
||||
@@ -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 (
|
||||
<div data-testid="location-state">
|
||||
{JSON.stringify(location.state || {})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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(
|
||||
<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 () => {
|
||||
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(
|
||||
<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,
|
||||
} 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<NavigateFunction>(navigate);
|
||||
const canvasRef = useRef<CanvasTools>(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 (
|
||||
<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="text-center space-y-6 z-10">
|
||||
<Logo />
|
||||
|
||||
Reference in New Issue
Block a user