Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { ManageProductionsPage } from "./components/manage-productions-page/mana
import { CreateProductionPage } from "./components/create-production/create-production-page.tsx";
import { useSetupTokenRefresh } from "./hooks/use-reauth.tsx";
import { TUserSettings } from "./components/user-settings/types";
import { RequireNonGuest } from "./components/auth/require-non-guest.tsx";
import { PresetProvider } from "./contexts/preset-context.tsx";

const DisplayBoxPositioningContainer = styled(FlexContainer)`
Expand Down Expand Up @@ -153,21 +154,29 @@ const AppContent = ({
<Route
path="/"
element={
<LandingPage setApiError={() => setApiError(true)} />
<RequireNonGuest>
<LandingPage setApiError={() => setApiError(true)} />
</RequireNonGuest>
}
errorElement={<ErrorPage />}
/>
<Route
path="/create"
element={<CreateProductionPage />}
element={
<RequireNonGuest>
<CreateProductionPage />
</RequireNonGuest>
}
errorElement={<ErrorPage />}
/>
<Route
path="/manage"
element={
<ManageProductionsPage
setApiError={() => setApiError(true)}
/>
<RequireNonGuest>
<ManageProductionsPage
setApiError={() => setApiError(true)}
/>
</RequireNonGuest>
}
errorElement={<ErrorPage />}
/>
Expand All @@ -181,7 +190,14 @@ const AppContent = ({
element={<CallsPage />}
errorElement={<ErrorPage />}
/>
<Route path="/lines" element={<LinesToCallsRedirect />} />
<Route
path="/lines"
element={
<RequireNonGuest>
<LinesToCallsRedirect />
</RequireNonGuest>
}
/>
<Route path="*" element={<NotFound />} />
</>
</Routes>
Expand Down
68 changes: 68 additions & 0 deletions src/components/auth/require-non-guest.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, expect, it, vi, afterEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { MemoryRouter, Route, Routes } from "react-router";
import { RequireNonGuest } from "./require-non-guest";

const mockUseIsGuest = vi.fn();
vi.mock("../../hooks/use-is-guest", () => ({
useIsGuest: () => mockUseIsGuest(),
}));

const mockGetPath = vi.fn();
vi.mock("../../utils/guest-session", () => ({
guestSession: {
getPath: () => mockGetPath(),
},
}));

afterEach(() => {
mockUseIsGuest.mockReset();
mockGetPath.mockReset();
});

const renderAt = (path: string) =>
render(
<MemoryRouter initialEntries={[path]}>
<Routes>
<Route
path="/"
element={
<RequireNonGuest>
<div>Landing page</div>
</RequireNonGuest>
}
/>
<Route path="/calls" element={<div>Calls page</div>} />
</Routes>
</MemoryRouter>
);

describe("RequireNonGuest", () => {
it("renders children for a normal user", () => {
mockUseIsGuest.mockReturnValue(false);

renderAt("/");

expect(screen.getByText("Landing page")).toBeInTheDocument();
});

it("redirects a guest to their invited path", () => {
mockUseIsGuest.mockReturnValue(true);
mockGetPath.mockReturnValue("/calls?lines=p:l&guest=1");

renderAt("/");

expect(screen.queryByText("Landing page")).toBeNull();
expect(screen.getByText("Calls page")).toBeInTheDocument();
});

it("falls back to /calls when no invited path was stored", () => {
mockUseIsGuest.mockReturnValue(true);
mockGetPath.mockReturnValue(null);

renderAt("/");

expect(screen.queryByText("Landing page")).toBeNull();
expect(screen.getByText("Calls page")).toBeInTheDocument();
});
});
18 changes: 18 additions & 0 deletions src/components/auth/require-non-guest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ReactNode } from "react";
import { Navigate } from "react-router";
import { useIsGuest } from "../../hooks/use-is-guest";
import { guestSession } from "../../utils/guest-session";

type RequireNonGuestProps = {
children: ReactNode;
};

export const RequireNonGuest = ({ children }: RequireNonGuestProps) => {
const isGuest = useIsGuest();

if (isGuest) {
return <Navigate to={guestSession.getPath() || "/calls"} replace />;
}

return children;
};
40 changes: 22 additions & 18 deletions src/components/calls-page/calls-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { PageHeader } from "../page-layout/page-header";
import { useAudioCue } from "../production-line/use-audio-cue";
import { useGlobalHotkeys } from "../production-line/use-line-hotkeys";
import { ShareUrlModal } from "../share-url-modal/share-url-modal";
import { useIsGuest } from "../../hooks/use-is-guest";
import { useInitiateProductionCall } from "../../hooks/use-initiate-production-call";
import { UserSettings } from "../user-settings/user-settings";
import { ConfirmationModal } from "../verify-decision/confirmation-modal";
Expand Down Expand Up @@ -155,6 +156,7 @@ export const CallsPage = () => {

const { productionId: paramProductionId, lineId: paramLineId } = useParams();
const { search } = useLocation();
const isGuest = useIsGuest();
const autoCompanionUrl = parseCompanionParam(
new URLSearchParams(search).get("companion")
);
Expand Down Expand Up @@ -452,7 +454,7 @@ export const CallsPage = () => {
<PageHeader
title={!isEmpty ? "Calls" : ""}
titleAdornment={
!isEmpty ? (
!isEmpty && !isGuest ? (
<ShareAdornment>
<CopyIconWrapper
title="Share lines URL"
Expand All @@ -465,7 +467,7 @@ export const CallsPage = () => {
</ShareAdornment>
) : undefined
}
hasNavigateToRoot
hasNavigateToRoot={!isGuest}
onNavigateToRoot={() => {
if (isEmpty) {
runExitAllCalls();
Expand Down Expand Up @@ -549,22 +551,24 @@ export const CallsPage = () => {
/>
)}
<CallsContainer>
{addCallActive && (productionId || addCallPreSelected) && (
<JoinProduction
customGlobalMute={customGlobalMute}
addAdditionalCallId={
productionId ??
addCallPreSelected?.preSelectedProductionId ??
""
}
prefetchedProduction={prefetchedProduction}
prefetchedProductionList={prefetchedProductionList}
closeAddCallView={() => setAddCallActive(false)}
className="calls-page"
hideUsername
hideDevices
/>
)}
{addCallActive &&
!isGuest &&
(productionId || addCallPreSelected) && (
<JoinProduction
customGlobalMute={customGlobalMute}
addAdditionalCallId={
productionId ??
addCallPreSelected?.preSelectedProductionId ??
""
}
prefetchedProduction={prefetchedProduction}
prefetchedProductionList={prefetchedProductionList}
closeAddCallView={() => setAddCallActive(false)}
className="calls-page"
hideUsername
hideDevices
/>
)}
{!!(
userSettings &&
userSettings.username &&
Expand Down
9 changes: 6 additions & 3 deletions src/components/calls-page/header-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MicMuted, MicUnmuted } from "../../assets/icons/icon";
import { isMobile, isTablet } from "../../bowser";
import { PrimaryButton, SecondaryButton } from "../form-elements/form-elements";
import { ConnectToWSButton } from "./connect-to-ws-button";
import { useIsGuest } from "../../hooks/use-is-guest";
import { useGlobalMuteToggle } from "./use-global-mute-toggle";
import { SavePresetModal } from "./save-preset-modal";
import { useGlobalState } from "../../global-state/context-provider";
Expand Down Expand Up @@ -104,6 +105,8 @@ export const HeaderActions = ({
setIsMasterInputMuted,
setIsSettingGlobalMute,
});
const isGuest = useIsGuest();

const [showPresetModal, setShowPresetModal] = useState(false);

const activeCompanionUrl =
Expand All @@ -123,7 +126,7 @@ export const HeaderActions = ({
{isMasterInputMuted ? <MicMuted /> : <MicUnmuted />}
</MuteAllCallsBtn>
)}
{!isEmpty && !isMobile && !isTablet && (
{!isEmpty && !isMobile && !isTablet && !isGuest && (
<ConnectToWSButton
callActionHandlers={callActionHandlers}
callIndexMap={callIndexMap}
Expand All @@ -135,12 +138,12 @@ export const HeaderActions = ({
onCompanionUrlChange={onCompanionUrlChange}
/>
)}
{!isEmpty && (
{!isEmpty && !isGuest && (
<SavePresetBtn type="button" onClick={() => setShowPresetModal(true)}>
{isMobile ? "Save" : "Save as Configuration"}
</SavePresetBtn>
)}
{!isEmpty && (
{!isEmpty && !isGuest && (
<AddCallContainer>
<SecondaryButton
type="button"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ShareLineLinkModal } from "./share-line-link-modal";

const renderModal = (onRefresh = vi.fn()) => {
render(
<ShareLineLinkModal
urls={["https://example.test/shared"]}
onRefresh={onRefresh}
onClose={vi.fn()}
/>
);
return onRefresh;
};

describe("ShareLineLinkModal", () => {
it("restricts recipients by default", () => {
renderModal();

expect(screen.getByRole("checkbox")).toBeChecked();
});

it("asks for an unrestricted link when the box is cleared", async () => {
const onRefresh = renderModal();

await userEvent.click(screen.getByRole("checkbox"));

expect(onRefresh).toHaveBeenCalledWith({ guest: false });
});

it("asks for a restricted link when the box is ticked again", async () => {
const onRefresh = renderModal();

await userEvent.click(screen.getByRole("checkbox"));
await userEvent.click(screen.getByRole("checkbox"));

expect(onRefresh).toHaveBeenLastCalledWith({ guest: true });
});
});
Loading
Loading