Skip to content

Commit

Permalink
Merge pull request #2083 from navikt/feature/webworker-login
Browse files Browse the repository at this point in the history
[FAGSYSTEM-304086] Flytte login logikk til webworker
  • Loading branch information
Jesperpaulsen authored Nov 16, 2023
2 parents db01ed8 + 560e201 commit c257f46
Show file tree
Hide file tree
Showing 12 changed files with 364 additions and 2 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 '../utils/hooks/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
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-på-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();
};
}
86 changes: 86 additions & 0 deletions src/login/WebWorkerCommunicator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { AuthIntropectionDTO } from '../utils/hooks/use-persistent-login';
import { LoginStateManager } from './LoginStateManager';
import { META_URL } from './metaUrl';
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() {
this.worker = new window.Worker(new URL('../loginWebWorker', META_URL));
}

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');
}
this.refreshToken();
return;
}
case 'LOGIN_STATE_UPDATE': {
if (!this.onLoginStateUpdate) {
throw new Error('WebWorker was not initialized before being called');
}
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;
2 changes: 2 additions & 0 deletions src/login/metaUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Jest doesn't support import.meta, so this is extracted here to mock it during tests.
export const META_URL = import.meta.url;
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;
}
43 changes: 43 additions & 0 deletions src/loginWebWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* eslint-disable no-restricted-globals */
import { LoginStateManager } from './login/LoginStateManager';
import { IncommingMessageType, OutgoingMessageType } from './login/types';

const loginStateManager = new LoginStateManager();

const register = () => {
console.log('Bruker webworker for å kontrollere inlogging');
self.addEventListener('message', handleEventMessage);
};

const handleEventMessage = (event: MessageEvent<{ type: IncommingMessageType; payload: any }>) => {
const { type, payload } = event.data;
switch (type) {
case 'STOP_WORKER':
loginStateManager.stopWork();
console.log('[loginWebWorker] Mottok melding: ', type);
return;
case 'AUTH_STATE_UPDATE':
loginStateManager.onUpdate(payload);
console.log(`[loginWebWorker] Mottok melding: ${type}, med: ${payload.expirationDate}`);
return;
case 'USER_ACTIVE':
loginStateManager.onUserActive();
return;
}
};

const sendRefreshMessage = () => {
sendMessage('REFRESH_TOKEN');
};

const sendIsLoggedIn = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
sendMessage('LOGIN_STATE_UPDATE', isLoggedIn);
};

const sendMessage = <T>(type: OutgoingMessageType, payload?: T) => {
self.postMessage({ type, payload });
};

loginStateManager.initialize(sendRefreshMessage, sendIsLoggedIn);

register();
4 changes: 4 additions & 0 deletions src/setupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,9 @@ jest.mock('react-collapse', () => {
};
});

jest.mock('./login/metaUrl.ts', () => ({
META_URL: 'https://mock.com'
}));

beforeEach(EnzymeContainer.beforeEachHandler);
afterEach(EnzymeContainer.afterEachHandler);
Loading

0 comments on commit c257f46

Please sign in to comment.