Skip to content

Commit

Permalink
Merge pull request #2081 from navikt/dev
Browse files Browse the repository at this point in the history
[PROD][KAIZEN-0] utbetalings period filter
  • Loading branch information
Jesperpaulsen authored Nov 17, 2023
2 parents bdad9ca + 0fa6301 commit d572b6f
Show file tree
Hide file tree
Showing 14 changed files with 391 additions and 4 deletions.
14 changes: 12 additions & 2 deletions src/app/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ import Routing from './Routing';
import styled from 'styled-components';
import { useOnMount } from '../utils/customHooks';
import VelgEnhet from './VelgEnhet';
import usePersistentLogin from '../utils/hooks/use-persistent-login';
import LoggetUtModal from './LoggetUtModal';
import { useValgtenhet, ValgtEnhetProvider } from '../context/valgtenhet-state';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { usePersistentWWLogin } from '../login/use-persistent-ww-login';
import usePersistentLogin from '../utils/hooks/use-persistent-login';
import useFeatureToggle from '../components/featureToggle/useFeatureToggle';
import { FeatureToggles } from '../components/featureToggle/toggleIDs';

const AppStyle = styled.div`
height: 100vh;
Expand All @@ -42,9 +45,16 @@ const ContentStyle = styled.div`
const store = createStore(reducers, composeWithDevTools(applyMiddleware(thunk)));

function App() {
const loginState = usePersistentLogin();
const loginStateOld = usePersistentLogin();
const loginStateNew = usePersistentWWLogin();
const { isOn: newLoginStateToggleIsOn } = useFeatureToggle(FeatureToggles.BrukWebworkerPaaInnLogging);
const valgtEnhet = useValgtenhet().enhetId;

let loginState = loginStateOld;
if (newLoginStateToggleIsOn) {
loginState = loginStateNew;
}

if (!valgtEnhet) {
/**
* valgt enhet hentes fra modiacontextholder, og mellomlagres i localStorage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ const UtbetalingerStyle = styled.div`
`;

const FiltreringSection = styled.section`
height: fit-content;
width: 100%;
@media (min-width: ${theme.media.utbetalinger.minWidth}) {
width: 19.5rem;
width: 22.5rem;
}
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,9 @@ exports[`Viser utbetalingercontainer med alt innhold 1`] = `
}
.c4 {
height: -webkit-fit-content;
height: -moz-fit-content;
height: fit-content;
width: 100%;
}
Expand Down Expand Up @@ -572,7 +575,7 @@ exports[`Viser utbetalingercontainer med alt innhold 1`] = `
@media (min-width:1328px) {
.c4 {
width: 19.5rem;
width: 22.5rem;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/components/featureToggle/toggleIDs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export enum FeatureToggles {
BrukSoknadsstatus = 'modiapersonoversikt.soknadsstatus-api',
BrukUtvidetUtbetalingsSporring = 'modiapersonoversikt.utvidet-utbetalings-sporring',
BrukWebworkerPaaInnLogging = 'modiapersonoversikt.web-worker-paa-innlogging',
DebugMeldingsFunksjonalitet = 'modiapersonoversikt.ny-send-melding-container'
}
11 changes: 11 additions & 0 deletions src/login/AcitivityMonitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class ActivityMonitor {
private lastActivity: number = new Date().getTime();

public update() {
this.lastActivity = new Date().getTime();
}

public timeSinceLastActivity(): number {
return new Date().getTime() - this.lastActivity;
}
}
100 changes: 100 additions & 0 deletions src/login/LoginStateManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { AuthIntropectionDTO } from '../utils/hooks/use-persistent-login';
import { ActivityMonitor } from './AcitivityMonitor';
import { INACTIVITY_LIMIT_IN_MS, PREEMPTIVE_REFRESH_TIME_IN_MS, RECALC_LOGIN_STATUS_INTERVAL_IN_MS } from './constants';
import { timeToExpiration } from './timeToExpiration';

export class LoginStateManager {
private timeout: ReturnType<typeof setTimeout> | null = null;
private interval: ReturnType<typeof setInterval> | null = null;
private activityMonitor = new ActivityMonitor();
private _refreshToken?: () => void;
private _onLoginStateUpdate?: (props: { isLoggedIn: boolean }) => void;

initialize = (refreshToken: () => void, onLoginStateUpdate: (props: { isLoggedIn: boolean }) => void) => {
this._refreshToken = refreshToken;
this._onLoginStateUpdate = onLoginStateUpdate;
};

private setupTokenRefresher = (timeToRefresh: number) => {
this.timeout = this.getTokenRefreshTimeout(this.activityMonitor, timeToRefresh);
};

private get refreshToken() {
if (!this._refreshToken) {
throw new Error('[LoginStateManager] var ikke initialisert med en metode for å refreshe token');
}
return this._refreshToken;
}

private get onLoginStateUpdate() {
if (!this._onLoginStateUpdate) {
throw new Error('[LoginStateManager] var ikke initialisert med en metode for å sende login oppdateringer');
}
return this._onLoginStateUpdate;
}

private getTokenRefreshTimeout = (activityMonitor: ActivityMonitor, timeToExpiration: number) => {
return setTimeout(() => {
if (activityMonitor.timeSinceLastActivity() < INACTIVITY_LIMIT_IN_MS) {
if (!this.refreshToken) {
throw new Error('[LoginStateManager] Var ikke initialisert med ');
}
if (this.refreshToken) {
this.refreshToken();
}
}
}, [timeToExpiration - PREEMPTIVE_REFRESH_TIME_IN_MS]);
};

private getLoginStateInterval = (auth: AuthIntropectionDTO) => {
return setInterval(() => {
const timeLeft = timeToExpiration(auth.expirationDate);
if (this.onLoginStateUpdate) {
this.onLoginStateUpdate({ isLoggedIn: timeLeft > 0 });
}
}, RECALC_LOGIN_STATUS_INTERVAL_IN_MS);
};

private onAuthStateUpdate = (auth: AuthIntropectionDTO) => {
this.stopTokenRefresher();
const timeToRefresh = timeToExpiration(auth.expirationDate);
if (timeToRefresh === 0) {
if (this.refreshToken) {
this.refreshToken();
}
return;
}
this.setupTokenRefresher(timeToRefresh);
};

private setupLoginStateNotifier = (auth: AuthIntropectionDTO) => {
this.stopLoginStateNotifier();
this.interval = this.getLoginStateInterval(auth);
};

private stopTokenRefresher = () => {
if (this.timeout) {
clearTimeout(this.timeout);
}
};

private stopLoginStateNotifier = () => {
if (this.interval) {
clearInterval(this.interval);
}
};

onUserActive = () => {
this.activityMonitor.update();
};

onUpdate = (auth: AuthIntropectionDTO) => {
this.onAuthStateUpdate(auth);
this.setupLoginStateNotifier(auth);
};

stopWork = () => {
this.stopTokenRefresher();
this.stopLoginStateNotifier();
};
}
87 changes: 87 additions & 0 deletions src/login/WebWorkerCommunicator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { AuthIntropectionDTO } from '../utils/hooks/use-persistent-login';
import { LoginStateManager } from './LoginStateManager';
import { WWMessage, OutgoingMessageType, IncommingMessageType } from './types';

export interface IWebWorkerCom {
initialize: (refreshToken: () => void, onLoginStateUpdate: (props: { isLoggedIn: boolean }) => void) => void;
onAuthChange: (newState: AuthIntropectionDTO) => void;
stop: () => void;
onUserActive: () => void;
}

export class WebWorkerCommunicator implements IWebWorkerCom {
private worker: Worker;
refreshToken?: () => void;
onLoginStateUpdate?: (props: { isLoggedIn: boolean }) => void;

constructor(worker: Worker) {
this.worker = worker;
}

initialize = (refreshToken: () => void, onLoginStateUpdate: (props: { isLoggedIn: boolean }) => void) => {
this.refreshToken = refreshToken;
this.onLoginStateUpdate = onLoginStateUpdate;
this.worker.onmessage = (message: MessageEvent<WWMessage<any>>) => {
const { type, payload } = message.data;
this.onMessage(type as OutgoingMessageType, payload);
};
};

private sendMessage = (type: IncommingMessageType, payload?: any) => {
const message: WWMessage<any> = {
type,
payload
};
this.worker.postMessage(message);
};

private onMessage = (type: OutgoingMessageType, payload?: any) => {
switch (type) {
case 'REFRESH_TOKEN': {
if (!this.refreshToken) {
throw new Error('WebWorker was not initialized before being called');
}
console.log(new Date().valueOf(), 'Refresh token message');
this.refreshToken();
return;
}
case 'LOGIN_STATE_UPDATE': {
if (!this.onLoginStateUpdate) {
throw new Error('WebWorker was not initialized before being called');
}
console.log(new Date().valueOf(), 'Login state update');
this.onLoginStateUpdate({ isLoggedIn: payload });
return;
}
}
};

onUserActive = () => {
this.sendMessage('USER_ACTIVE');
};

onAuthChange = (newState: AuthIntropectionDTO) => {
this.sendMessage('AUTH_STATE_UPDATE', newState);
};

stop = () => this.sendMessage('STOP_WORKER');
}

export class NoWorkerCommunicator implements IWebWorkerCom {
private loginStateManager = new LoginStateManager();

initialize = (refreshToken: () => void, onLoginStateUpdate: (props: { isLoggedIn: boolean }) => void) => {
this.loginStateManager.initialize(refreshToken, onLoginStateUpdate);
};

onAuthChange = (newState: AuthIntropectionDTO) => {
this.loginStateManager.onUpdate(newState);
};
stop = () => {
this.loginStateManager.stopWork();
};

onUserActive = () => {
this.loginStateManager.onUserActive();
};
}
8 changes: 8 additions & 0 deletions src/login/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const SECOND_IN_MS = 1000;
export const MINUTE_IN_MS = 60 * SECOND_IN_MS;

export const RECALC_LOGIN_STATUS_INTERVAL_IN_MS = 30 * SECOND_IN_MS;
export const INACTIVITY_LIMIT_IN_MS = 10 * MINUTE_IN_MS;
export const PREEMPTIVE_REFRESH_TIME_IN_MS = 120 * SECOND_IN_MS;
export const ESTIMATED_EXPIRATION_IN_MS = 3600 * SECOND_IN_MS;
export const INVALID_EXPIRATION_DATE = -1;
16 changes: 16 additions & 0 deletions src/login/persistentLoginWebWorkerFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IWebWorkerCom, NoWorkerCommunicator, WebWorkerCommunicator } from './WebWorkerCommunicator';

export const persistentLoginWebworkerFactory = (): IWebWorkerCom => {
if (!Worker) {
console.warn('WebWorker er ikke støttet av nettleseren. Kjører innlogging logikk i hovedtråden.');
return new NoWorkerCommunicator();
}
let worker: Worker;
try {
worker = new Worker(new URL('../loginWebWorker', import.meta.url));
return new WebWorkerCommunicator(worker);
} catch (e) {
console.log(e);
return new NoWorkerCommunicator();
}
};
9 changes: 9 additions & 0 deletions src/login/timeToExpiration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ESTIMATED_EXPIRATION_IN_MS, INVALID_EXPIRATION_DATE } from './constants';

export const timeToExpiration = (expirationDate: number): number => {
if (expirationDate === INVALID_EXPIRATION_DATE) {
return ESTIMATED_EXPIRATION_IN_MS;
}
const currentDate = new Date().getTime();
return expirationDate - currentDate;
};
6 changes: 6 additions & 0 deletions src/login/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type OutgoingMessageType = 'LOGIN_STATE_UPDATE' | 'REFRESH_TOKEN';
export type IncommingMessageType = 'AUTH_STATE_UPDATE' | 'STOP_WORKER' | 'USER_ACTIVE';
export interface WWMessage<T> {
type: IncommingMessageType | OutgoingMessageType;
payload: T;
}
74 changes: 74 additions & 0 deletions src/login/use-persistent-ww-login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useEffect, useMemo, useState } from 'react';
import {
AuthIntropectionDTO,
ErrorReason,
INVALID_EXPIRATION_DATE,
PersistentLoginState
} from '../utils/hooks/use-persistent-login';
import { UseQueryResult, useQuery } from '@tanstack/react-query';
import { FetchError, get } from '../api/api';
import { apiBaseUri } from '../api/config';
import { persistentLoginWebworkerFactory } from './persistentLoginWebWorkerFactory';

const authResource = {
useFetch(): UseQueryResult<AuthIntropectionDTO, FetchError> {
return useQuery(['auth'], () => get(`${apiBaseUri}/tilgang/auth`));
}
};

const errorHandling = (auth: UseQueryResult<AuthIntropectionDTO, FetchError>): ErrorReason | undefined => {
if (auth.isError) {
return ErrorReason.FETCH_ERROR;
} else if (auth.data && auth.data.expirationDate === INVALID_EXPIRATION_DATE) {
return ErrorReason.INVALID_EXPIRATION_DATE;
}
return undefined;
};

const useAuthStateLogin = (auth: UseQueryResult<AuthIntropectionDTO, FetchError>) => {
const [isLoggedIn, setIsLoggedIn] = useState(true);
const [webWorkerCom] = useState(() => {
const worker = persistentLoginWebworkerFactory();
worker.initialize(auth.refetch, ({ isLoggedIn }) => setIsLoggedIn(isLoggedIn));
return worker;
});

useEffect(() => {
if (webWorkerCom) {
document.addEventListener('mousemove', webWorkerCom.onUserActive);
document.addEventListener('keydown', webWorkerCom.onUserActive);
}
return () => {
if (webWorkerCom) {
document.removeEventListener('mousemove', webWorkerCom.onUserActive);
document.removeEventListener('keydown', webWorkerCom.onUserActive);
}
};
}, [webWorkerCom]);

useEffect(() => {
if (auth.status === 'success') {
webWorkerCom.onAuthChange(auth.data);
return;
} else if (auth.status === 'error') {
setIsLoggedIn(false);
webWorkerCom.stop();
}
}, [auth, webWorkerCom]);

return isLoggedIn;
};

export const usePersistentWWLogin = (): PersistentLoginState => {
const auth = authResource.useFetch();
const errorStatus = errorHandling(auth);
const isLoggedIn = useAuthStateLogin(auth);

return useMemo(
() => ({
isLoggedIn,
errorStatus
}),
[isLoggedIn, errorStatus]
);
};
Loading

0 comments on commit d572b6f

Please sign in to comment.