Skip to content

Commit

Permalink
Fix session click tracker (#233)
Browse files Browse the repository at this point in the history
* Add SessionExpirationTracker component

* stores: update zustand import

* Create wrapping AccountContext component. Makes it easier to use the accountStore (see https://docs.pmnd.rs/zustand/guides/initialize-state-with-props)
  • Loading branch information
mold authored Feb 14, 2023
1 parent e6880b1 commit 665b0bc
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 124 deletions.
60 changes: 60 additions & 0 deletions src/components/SessionExpirationTracker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { refresh } from "@jaws/auth/firestoreAuth";
import { ONE_HOUR_IN_MS, SESSION_LENGTH_IN_MS } from "@jaws/lib/helpers";
import { useAccountStore } from "@jaws/store/account/accountContext";
import { setCookies } from "cookies-next";
import { useState } from "react";
import { useClickAnyWhere, useInterval } from "usehooks-ts";

export interface Props {
children?: any;
}

export const SessionExpirationTracker = ({ children }: Props) => {
const [interval, setInterval] = useState(SESSION_LENGTH_IN_MS);
const [user, setUser, logoutUser] = useAccountStore((state) => [
state.user,
state.setUser,
state.logoutUser,
]);

let latestInteraction = Date.now();
useClickAnyWhere(() => {
latestInteraction = Date.now();
setInterval(0);

async function doRefresh() {
if (
!user ||
(user.sessionExpires &&
user.sessionExpires - Date.now() > SESSION_LENGTH_IN_MS)
) {
// Only refresh token if less then SESSION_LENGTH_IN_MS left
return;
}

try {
const newToken = await refresh();
if (newToken) {
setCookies("idToken", newToken);
setUser({
...user,
sessionExpires: Date.now() + ONE_HOUR_IN_MS,
});
}
} catch (err) {
console.log("Refresh token failed", JSON.stringify(err));
}
}
void doRefresh();
});

useInterval(() => {
setInterval(SESSION_LENGTH_IN_MS);
if (user && Date.now() - latestInteraction > SESSION_LENGTH_IN_MS) {
// Session time expired - lets logout
logoutUser();
return;
}
}, interval);
return <>{children}</>;
};
44 changes: 44 additions & 0 deletions src/components/molecules/AppWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useAccountStore } from "@jaws/store/account/accountContext";
import { User } from "@jaws/store/account/accountStore";
import { AppProps } from "next/app";
import { ModalProvider } from "use-modal-hook";
import ErrorMessage from "../atoms/ErrorMessage";
import PageContainer from "../atoms/PageContainer";
import Navbar from "../organisms/Navbar";

export type AuthError = {
message: string;
code: string;
};
export interface ExtendedAppProps extends AppProps {
pageProps: {
authedUser: User | null;
authError?: AuthError;
};
}

export const AppWrapper = ({ Component, ...appProps }: ExtendedAppProps) => {
const { authError } = appProps.pageProps;

const isLoggedIn = useAccountStore((state) => state.isLoggedIn);

return (
<>
<Navbar />
{isLoggedIn ? (
<ModalProvider>
<Component {...appProps.pageProps} />
</ModalProvider>
) : (
<PageContainer>
<h1>You need to Log in</h1>
{authError && (
<ErrorMessage>
{authError.message} {authError.code && `(${authError.code})`}
</ErrorMessage>
)}
</PageContainer>
)}
</>
);
};
16 changes: 8 additions & 8 deletions src/components/organisms/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import Link from "next/link";
import React, { useContext, useEffect, useState } from "react";
import styled, { css } from "styled-components";
import { signInWithGoogle } from "@jaws/auth/firestoreAuth";
import { getToday } from "@jaws/lib/helpers";
import { AccountContext } from "@jaws/store/account/accountContext";
import { User } from "@jaws/store/account/accountStore";
import { Theme } from "@jaws/styles/themes";
import { useRouter } from "next/router";
import { User } from "@jaws/store/accountStore";
import { signInWithGoogle } from "@jaws/auth/firestoreAuth";
import Button from "../atoms/buttons/Button";
import { setCookies } from "cookies-next";
import Link from "next/link";
import { useRouter } from "next/router";
import { useContext, useEffect, useState } from "react";
import styled, { css } from "styled-components";
import { useStore } from "zustand";
import { AccountContext } from "@jaws/store/accountContext";
import Button from "../atoms/buttons/Button";

const NavBarContainer = styled.div`
display: flex;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/getServerSidePropsAllPages.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getCookie } from "cookies-next";
import { auth } from "@jaws/auth/firebaseAdmin";
import { User } from "@jaws/store/accountStore";
import { User } from "@jaws/store/account/accountStore";
import { getCookie } from "cookies-next";

export const getServerSidePropsAllPages = async ({
req,
Expand Down
121 changes: 16 additions & 105 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
import type { AppProps } from "next/app";
import Head from "next/head";
import "@jaws/styles/globals.css";
import initializeFirebase from "@jaws/auth/initializeFirebase";
import {
AppWrapper,
ExtendedAppProps,
} from "@jaws/components/molecules/AppWrapper";
import { SessionExpirationTracker } from "@jaws/components/SessionExpirationTracker";
import { AccountProvider } from "@jaws/store/account/AccountProvider";
import "@jaws/styles/fonts.css";
import "@jaws/styles/globals.css";
import themes from "@jaws/styles/themes";
import Head from "next/head";
import { createGlobalStyle, ThemeProvider } from "styled-components";
import { ModalProvider } from "use-modal-hook";
import Navbar from "@jaws/components/organisms/Navbar";
import initializeFirebase from "@jaws/auth/initializeFirebase";
import { createAccountStore, User } from "@jaws/store/accountStore";
import PageContainer from "@jaws/components/atoms/PageContainer";
import { useRef, useState } from "react";
import { AccountContext } from "@jaws/store/accountContext";
import { useStore } from "zustand";
import ErrorMessage from "@jaws/components/atoms/ErrorMessage";
import { useClickAnyWhere, useInterval } from "usehooks-ts";
import { refresh } from "@jaws/auth/firestoreAuth";
import { setCookies } from "cookies-next";
import {
ONE_HOUR_IN_MS,
ONE_MINUTE_IN_MS,
SESSION_LENGTH_IN_MS,
TEN_MINUTES_IN_MS,
} from "../lib/helpers";

const theme = themes.dark; // I know, we are now removing ability to switch theme without hard reload, but what the hell...

Expand All @@ -36,75 +24,10 @@ const GlobalStyle = createGlobalStyle`
}
`;

type AuthError = {
message: string;
code: string;
};
interface ExtendedAppProps extends AppProps {
pageProps: {
authedUser: User | null;
authError?: AuthError;
};
}

initializeFirebase();

function MyApp({ Component, pageProps }: ExtendedAppProps) {
const { authedUser, authError } = pageProps;
const store = useRef(
createAccountStore({
user: authedUser,
isLoggedIn: !!authedUser,
}),
).current;
const [interval, setInterval] = useState(SESSION_LENGTH_IN_MS);
const isLoggedIn = useStore(store, (state) => state.isLoggedIn);
const [user, setUser, logoutUser] = useStore(store, (state) => [
state.user,
state.setUser,
state.logoutUser,
]);

// TODO: This interval could be extracted in aseparate hook or such, so that this file gets cleaner
let latestInteraction = Date.now();
useClickAnyWhere(() => {
latestInteraction = Date.now();
setInterval(0);

async function doRefresh() {
if (
!user ||
(user.sessionExpires &&
user.sessionExpires - Date.now() > SESSION_LENGTH_IN_MS)
) {
// Only refresh token if less then SESSION_LENGTH_IN_MS left
return;
}

try {
const newToken = await refresh();
if (newToken) {
setCookies("idToken", newToken);
setUser({
...user,
sessionExpires: Date.now() + ONE_HOUR_IN_MS,
});
}
} catch (err) {
console.log("Refresh token failed", JSON.stringify(err));
}
}
void doRefresh();
});

useInterval(() => {
setInterval(SESSION_LENGTH_IN_MS);
if (user && Date.now() - latestInteraction > SESSION_LENGTH_IN_MS) {
// Session time expired - lets logout
logoutUser();
return;
}
}, interval);
function MyApp(appProps: ExtendedAppProps) {
const { authedUser } = appProps.pageProps;

return (
<>
Expand All @@ -123,23 +46,11 @@ function MyApp({ Component, pageProps }: ExtendedAppProps) {
></script>
</Head>
<ThemeProvider theme={theme}>
<AccountContext.Provider value={store}>
<Navbar />
{isLoggedIn ? (
<ModalProvider>
<Component {...pageProps} />
</ModalProvider>
) : (
<PageContainer>
<h1>You need to Log in</h1>
{authError && (
<ErrorMessage>
{authError.message} {authError.code && `(${authError.code})`}
</ErrorMessage>
)}
</PageContainer>
)}
</AccountContext.Provider>
<AccountProvider user={authedUser}>
<SessionExpirationTracker>
<AppWrapper {...appProps} />
</SessionExpirationTracker>
</AccountProvider>
</ThemeProvider>
<GlobalStyle />
</>
Expand Down
27 changes: 27 additions & 0 deletions src/store/account/AccountProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { PropsWithChildren, useRef } from "react";
import { AccountContext } from "./accountContext";
import { AccountStore, createAccountStore, User } from "./accountStore";

type AccountProviderProps = PropsWithChildren<{
user: User | null;
}>;

export const AccountProvider = ({
children,
...props
}: AccountProviderProps) => {
const storeRef = useRef<AccountStore>();

if (!storeRef.current) {
storeRef.current = createAccountStore({
user: props.user,
isLoggedIn: !!props.user,
});
}

return (
<AccountContext.Provider value={storeRef.current}>
{children}
</AccountContext.Provider>
);
};
17 changes: 17 additions & 0 deletions src/store/account/accountContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createContext, useContext } from "react";
import { useStore } from "zustand";
import { AccountState, AccountStore } from "./accountStore";

export const AccountContext = createContext<AccountStore | null>(null);

export const useAccountStore = <T>(
selector: (state: AccountState) => T,
equalityFn?: (left: T, right: T) => boolean,
) => {
const store = useContext(AccountContext);
if (!store) {
throw new Error("Missing AccountContext.Provider in component tree");
}

return useStore(store, selector, equalityFn);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createStore } from "zustand";
import { deleteCookie } from "cookies-next";
import { signOutUser } from "@jaws/auth/firestoreAuth";
import { deleteCookie } from "cookies-next";
import { createStore } from "zustand";

export type User = {
userId: string;
Expand Down
4 changes: 0 additions & 4 deletions src/store/accountContext.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/store/breakoutsStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import create from "zustand";
import { create } from "zustand";

export type BreakoutStoreType = {
image: string;
Expand Down
4 changes: 2 additions & 2 deletions src/store/tradesStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import create from "zustand";
import { TRADE_STATUS, TRADE_SIDE } from "@jaws/db/tradesMeta";
import { TRADE_SIDE, TRADE_STATUS } from "@jaws/db/tradesMeta";
import { create } from "zustand";

export type TradesStoreType = {
ticker: string;
Expand Down

0 comments on commit 665b0bc

Please sign in to comment.