Fix user authentication flow (#4)

* display site name

* fix user auth flow
This commit is contained in:
Florian Herrengt 2023-08-07 12:30:33 +01:00 committed by GitHub
parent 37a74aca5c
commit 3f48992c0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 2824 additions and 26 deletions

2716
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -44,10 +44,14 @@
"@storybook/react-vite": "^7.1.0", "@storybook/react-vite": "^7.1.0",
"@storybook/testing-library": "^0.2.0", "@storybook/testing-library": "^0.2.0",
"@storybook/types": "^7.1.0", "@storybook/types": "^7.1.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.0.0",
"@types/node": "^20.4.2", "@types/node": "^20.4.2",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"jsdom": "^22.1.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"storybook": "^7.1.0", "storybook": "^7.1.0",
"storybook-builder-vite": "^0.1.23" "storybook-builder-vite": "^0.1.23",
"vitest": "^0.34.1"
} }
} }

27
src/App.test.tsx Normal file
View File

@ -0,0 +1,27 @@
import { act, render } from "@testing-library/react";
import React from "react";
import { describe, test, vi } from "vitest";
import { App } from "./App";
import { config } from "./config";
vi.mock("./pages/MetricDetailsPage", () => ({
MetricDetailsPage: () => <div>MetricDetailsPage</div>,
}));
describe("App", () => {
test("Display metrics after the auth process", async () => {
const { queryByText } = render(<App />);
expect(queryByText("MetricDetailsPage")).toBeNull();
const mockEvent = new MessageEvent("message", {
data: "token",
origin: config.baseUrl,
});
await act(() => window.dispatchEvent(mockEvent));
expect(queryByText("MetricDetailsPage")).toBeDefined();
});
test("Display metrics if user is already authenticated", async () => {
localStorage.setItem(config.localstorageKeys.token, "token");
const { queryByText } = render(<App />);
expect(queryByText("MetricDetailsPage")).toBeDefined();
});
});

View File

@ -1,14 +1,17 @@
import { ApolloProvider } from "@apollo/client"; import { ApolloProvider } from "@apollo/client";
import React from "react"; import React, { useState } from "react";
import { apolloClient } from "./apolloClient"; import { apolloClient } from "./apolloClient";
import { AuthIFrame } from "./components/AuthIFrame"; import { AuthIFrame } from "./components";
import { MetricDetailsPage } from "./pages"; import { MetricDetailsPage } from "./pages/MetricDetailsPage";
export const App: React.FC = () => { export const App: React.FC = () => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean>();
return ( return (
<ApolloProvider client={apolloClient}> <ApolloProvider client={apolloClient}>
<AuthIFrame /> <AuthIFrame
<MetricDetailsPage /> onChange={(value) => setIsAuthenticated(value.isAuthenticated)}
/>
{isAuthenticated ? <MetricDetailsPage /> : null}
</ApolloProvider> </ApolloProvider>
); );
}; };

View File

@ -0,0 +1,37 @@
import { act, render } from "@testing-library/react";
import React from "react";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { AuthIFrame } from "..";
import { config } from "../../config";
describe("AuthIFrame", () => {
let mockLocalStorage;
beforeEach(() => {
mockLocalStorage = {
getItem: vi.fn(),
setItem: vi.fn(),
};
Object.defineProperty(window, "localStorage", { value: mockLocalStorage });
});
test("should set isAuthenticated to true when message received", async () => {
const onChange = vi.fn();
render(<AuthIFrame onChange={onChange} />);
expect(onChange).toHaveBeenCalledWith({ isAuthenticated: false });
const mockEvent = new MessageEvent("message", {
data: "token",
origin: config.baseUrl,
});
await act(() => window.dispatchEvent(mockEvent));
expect(onChange).toHaveBeenCalledWith({ isAuthenticated: true });
expect(window.localStorage.setItem).toHaveBeenCalledWith(
config.localstorageKeys.token,
"token"
);
});
});

View File

@ -1,5 +1,9 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { config } from "../config"; import { config } from "../../config";
interface AuthIFrameProps {
onChange(value: { isAuthenticated: boolean }): void;
}
/** /**
* This is based on Rodney Urquhart's example https://github.com/RodneyU215 * This is based on Rodney Urquhart's example https://github.com/RodneyU215
@ -11,13 +15,14 @@ import { config } from "../config";
* *
* This component listens to that event and save the token in local storage * This component listens to that event and save the token in local storage
*/ */
export const AuthIFrame: React.FC = () => { export const AuthIFrame: React.FC<AuthIFrameProps> = (props) => {
const [url, setUrl] = useState<string>(); const [url, setUrl] = useState<string>();
const [done, setDone] = useState<boolean>(false); const [done, setDone] = useState<boolean>(false);
const handleMessage = (event: MessageEvent) => { const handleMessage = (event: MessageEvent) => {
if (event.origin.startsWith(config.baseUrl)) { if (event.origin.startsWith(config.baseUrl)) {
localStorage.setItem(config.localstorageKeys.token, event.data); localStorage.setItem(config.localstorageKeys.token, event.data);
props.onChange({ isAuthenticated: true });
console.info( console.info(
`new token received from ${config.authIFrameUrl}`, `new token received from ${config.authIFrameUrl}`,
event.data event.data
@ -32,6 +37,11 @@ export const AuthIFrame: React.FC = () => {
useEffect(() => { useEffect(() => {
window.addEventListener("message", handleMessage); window.addEventListener("message", handleMessage);
props.onChange({
isAuthenticated: Boolean(
localStorage.getItem(config.localstorageKeys.token)
),
});
setUrl(`${config.authIFrameUrl}`); setUrl(`${config.authIFrameUrl}`);
return () => { return () => {
window.removeEventListener("message", handleMessage); window.removeEventListener("message", handleMessage);

View File

@ -0,0 +1 @@
export * from "./AuthIFrame";

View File

@ -2,3 +2,4 @@ export * from "./Button";
export * from "./Chart"; export * from "./Chart";
export * from "./Dropdown"; export * from "./Dropdown";
export * from "./Icon"; export * from "./Icon";
export * from "./AuthIFrame";

View File

@ -1,12 +1,14 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Dropdown } from "../components"; import { Dropdown } from "../components";
import { Chart } from "../containers/Chart"; import { Chart } from "../containers/Chart";
import { MetricType } from "../generated/graphql"; import { MetricType, MetricTimeframe } from "../generated/graphql";
export const MetricDetailsPage: React.FC = () => { export const MetricDetailsPage: React.FC = () => {
const [path, setPath] = useState<string>(); const [path, setPath] = useState<string>();
const [name, setName] = useState<string>(); const [name, setName] = useState<string>();
const [timeframe, setTimeframe] = useState<string>("Last30days"); const [timeframe, setTimeframe] = useState<MetricTimeframe>(
MetricTimeframe.Last30days
);
useEffect(() => { useEffect(() => {
const unsubscribeCurrentPage = webflow.subscribe("currentpage", (page) => { const unsubscribeCurrentPage = webflow.subscribe("currentpage", (page) => {
@ -38,14 +40,14 @@ export const MetricDetailsPage: React.FC = () => {
<Dropdown <Dropdown
selected="Last30days" selected="Last30days"
options={[ options={[
{ label: "Last 7 days", value: "Last7days" }, { label: "Last 7 days", value: MetricTimeframe.Last7days },
{ label: "Last 30 days", value: "Last30days" }, { label: "Last 30 days", value: MetricTimeframe.Last30days },
{ label: "Last 3 Months", value: "Last3Months" }, { label: "Last 3 Months", value: MetricTimeframe.Last3Months },
{ label: "Last 6 Months", value: "Last6Months" }, { label: "Last 6 Months", value: MetricTimeframe.Last6Months },
{ label: "Last Year", value: "LastYear" }, { label: "Last Year", value: MetricTimeframe.LastYear },
]} ]}
onChange={(value) => { onChange={(value) => {
setTimeframe(value); setTimeframe(value as MetricTimeframe);
}} }}
/> />
</div> </div>

4
src/setupTests.ts Normal file
View File

@ -0,0 +1,4 @@
import "@testing-library/jest-dom";
import "cross-fetch/polyfill";
process.env.BASE_URL = window.location.origin;

13
vitest.config.ts Normal file
View File

@ -0,0 +1,13 @@
/// <reference types="vitest" />
/// <reference types="vite/client" />
import { defineConfig } from "vitest/config";
// eslint-disable-next-line import/no-default-export
export default defineConfig({
test: {
globals: true,
setupFiles: "./src/setupTests.ts",
environment: "jsdom",
css: false,
},
});