Skip to content

Commit

Permalink
Add auto logout middleware (#9)
Browse files Browse the repository at this point in the history
Add logout
  • Loading branch information
arhtudormorar authored Jul 17, 2024
1 parent df42dda commit 327406c
Show file tree
Hide file tree
Showing 24 changed files with 607 additions and 101 deletions.
6 changes: 1 addition & 5 deletions src/core/ProviderFactory.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { Transaction } from '@multiversx/sdk-core';
import {
// IframeProvider,
CrossWindowProvider
// ICrossWindowWalletAccount
} from '@multiversx/sdk-web-wallet-cross-window-provider/out/CrossWindowProvider/CrossWindowProvider';
import { CrossWindowProvider } from 'lib/sdkWebWalletCrossWindowProvider';

export interface IProvider {
login: (options?: { token?: string }) => Promise<any>;
Expand Down
4 changes: 3 additions & 1 deletion src/core/methods/account/getIsLoggedIn.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { isLoggedInSelector } from 'store/selectors/accountSelectors';
import { getAddress } from './getAddress';
import { getState } from 'store/store';

export function getIsLoggedIn() {
return Boolean(getAddress());
return isLoggedInSelector(getState());
}
9 changes: 9 additions & 0 deletions src/core/methods/account/getWebviewToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { getWindowLocation } from 'utils/window/getWindowLocation';

export const getWebviewToken = () => {
const { search } = getWindowLocation();
const urlSearchParams = new URLSearchParams(search) as any;
const searchParams = Object.fromEntries(urlSearchParams);

return searchParams?.accessToken;
};
2 changes: 1 addition & 1 deletion src/core/methods/login/webWalletLogin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { LoginMethodsEnum } from 'types/enums.types';
import { OnProviderLoginType } from 'types/login.types';
import { CrossWindowProvider } from '@multiversx/sdk-web-wallet-cross-window-provider/out/CrossWindowProvider/CrossWindowProvider';
import { getWindowLocation } from 'utils/window/getWindowLocation';
import { getLoginService } from './helpers/getLoginService';
import { networkSelector } from 'store/selectors';
Expand All @@ -14,6 +13,7 @@ import { loginAction } from 'store/actions/sharedActions';
import { setAccount } from 'store/actions/account/accountActions';
import { getLatestNonce } from 'utils/account/getLatestNonce';
import { AccountType } from 'types/account.types';
import { CrossWindowProvider } from 'lib/sdkWebWalletCrossWindowProvider';

export const webWalletLogin = async ({
token: tokenToSign,
Expand Down
72 changes: 72 additions & 0 deletions src/core/methods/logout/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { storage } from 'storage';
import { localStorageKeys } from 'storage/local';
import { LoginMethodsEnum } from 'types';
import { getAddress } from '../account/getAddress';
import { CrossWindowProvider } from 'lib/sdkWebWalletCrossWindowProvider';
import { logoutAction } from 'store/actions/sharedActions/sharedActions';
import { getWebviewToken } from '../account/getWebviewToken';
import { getAccountProvider } from 'core/providers/accountProvider';
import { getProviderType } from 'core/providers/helpers/utils';

const broadcastLogoutAcrossTabs = (address: string) => {
const storedData = storage.local?.getItem(localStorageKeys.logoutEvent);
const { data } = storedData ? JSON.parse(storedData) : { data: address };

if (address !== data) {
return;
}

storage.local.setItem({
key: localStorageKeys.logoutEvent,
data: address,
expires: 0
});

storage.local.removeItem(localStorageKeys.logoutEvent);
};

export type LogoutPropsType = {
shouldAttemptReLogin?: boolean;
shouldBroadcastLogoutAcrossTabs?: boolean;
/*
* Only used for web-wallet crossWindow login
*/
hasConsentPopup?: boolean;
};

export async function logout(
shouldAttemptReLogin = Boolean(getWebviewToken()),
options = {
shouldBroadcastLogoutAcrossTabs: true,
hasConsentPopup: false
}
) {
let address = getAddress();
const provider = getAccountProvider();
const providerType = getProviderType(provider);

if (shouldAttemptReLogin && provider?.relogin != null) {
return provider.relogin();
}

if (options.shouldBroadcastLogoutAcrossTabs) {
broadcastLogoutAcrossTabs(address);
}

try {
logoutAction();

if (
options.hasConsentPopup &&
providerType === LoginMethodsEnum.crossWindow
) {
(provider as unknown as CrossWindowProvider).setShouldShowConsentPopup(
true
);
}

await provider.logout();
} catch (err) {
console.error('Logging out error:', err);
}
}
2 changes: 1 addition & 1 deletion src/core/providers/accountProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CrossWindowProvider } from '@multiversx/sdk-web-wallet-cross-window-provider/out/CrossWindowProvider/CrossWindowProvider';
import { IDappProvider } from 'types/dappProvider.types';
import { emptyProvider } from './helpers/emptyProvider';
import { CrossWindowProvider } from 'lib/sdkWebWalletCrossWindowProvider';

export type ProvidersType = IDappProvider | CrossWindowProvider;

Expand Down
2 changes: 1 addition & 1 deletion src/core/providers/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { ExtensionProvider } from '@multiversx/sdk-extension-provider';
import { HWProvider } from '@multiversx/sdk-hw-provider';
import { MetamaskProvider } from '@multiversx/sdk-metamask-provider/out/metamaskProvider';
import { OperaProvider } from '@multiversx/sdk-opera-provider';
import { CrossWindowProvider } from '@multiversx/sdk-web-wallet-cross-window-provider/out/CrossWindowProvider/CrossWindowProvider';
import { WalletProvider } from '@multiversx/sdk-web-wallet-provider';
import { LoginMethodsEnum } from 'types/enums.types';
import { WalletConnectV2Provider } from 'utils/walletconnect/__sdkWalletconnectProvider';
import { EmptyProvider } from './emptyProvider';
import { CrossWindowProvider } from 'lib/sdkWebWalletCrossWindowProvider';

export const getProviderType = <TProvider extends object>(
provider?: TProvider | null
Expand Down
1 change: 1 addition & 0 deletions src/lib/sdkWebWalletCrossWindowProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CrossWindowProvider } from '@multiversx/sdk-web-wallet-cross-window-provider/out/CrossWindowProvider/CrossWindowProvider';
4 changes: 4 additions & 0 deletions src/storage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import * as local from './local';
import * as session from './session';

export const storage = { session, local };
80 changes: 80 additions & 0 deletions src/storage/local.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { getUnixTimestamp } from 'utils/dateTime';

export const localStorageKeys = {
loginExpiresAt: 'sdk-dapp-login-expires-at',
logoutEvent: 'sdk-dapp-logout-event'
} as const;

type LocalValueType = keyof typeof localStorageKeys;
type LocalKeyType = typeof localStorageKeys[LocalValueType];

type ExpiresType = number | false;

const hasLocalStorage = typeof localStorage !== 'undefined';

export const setItem = ({
key,
data,
expires
}: {
key: LocalKeyType;
data: any;
expires: ExpiresType;
}) => {
if (!hasLocalStorage) {
return;
}
localStorage.setItem(
String(key),
JSON.stringify({
expires,
data
})
);
};

export const getItem = (key: LocalKeyType): any => {
if (!hasLocalStorage) {
return;
}
const item = localStorage.getItem(String(key));
if (!item) {
return null;
}

const deserializedItem = JSON.parse(item);
if (!deserializedItem) {
return null;
}

if (
!deserializedItem.hasOwnProperty('expires') ||
!deserializedItem.hasOwnProperty('data')
) {
return null;
}

const expired = getUnixTimestamp() >= deserializedItem.expires;
if (expired) {
localStorage.removeItem(String(key));
return null;
}

return deserializedItem.data;
};

export const removeItem = (key: LocalKeyType) => {
if (!hasLocalStorage) {
return;
}

localStorage.removeItem(String(key));
};

export const clear = () => {
if (!hasLocalStorage) {
return;
}

localStorage.clear();
};
57 changes: 57 additions & 0 deletions src/storage/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export type SessionKeyType = 'address' | 'shard' | 'toasts' | 'toastProgress';
type ExpiresType = number | false;

export interface SetItemType {
key: SessionKeyType;
data: any;
expires: ExpiresType;
}

export const setItem = ({ key, data, expires }: SetItemType) => {
sessionStorage.setItem(
String(key),
JSON.stringify({
expires,
data
})
);
};

export const getItem = (key: SessionKeyType): any => {
const item = sessionStorage.getItem(String(key));
if (!item) {
return null;
}

const deserializedItem = JSON.parse(item);
if (!deserializedItem) {
return null;
}

if (
!deserializedItem.hasOwnProperty('expires') ||
!deserializedItem.hasOwnProperty('data')
) {
return null;
}

const expired = Date.now() >= deserializedItem.expires;
if (expired) {
sessionStorage.removeItem(String(key));
return null;
}

return deserializedItem.data;
};

export const removeItem = (key: SessionKeyType) =>
sessionStorage.removeItem(String(key));

export const clear = () => sessionStorage.clear();

export const storage = {
setItem,
getItem,
removeItem,
clear
};
11 changes: 2 additions & 9 deletions src/store/actions/sharedActions/sharedActions.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { Address } from '@multiversx/sdk-core/out';
import { initialState as initialAccountState } from 'store/slices/account/accountSlice';
import { initialState as initialLoginInfoState } from 'store/slices/loginInfo/loginInfoSlice';
import { store } from '../../store';
import { LoginMethodsEnum } from 'types/enums.types';
import { resetStore } from 'store/middleware/logoutMiddleware';

export const logoutAction = () =>
store.setState((store) => {
store.account = initialAccountState;
store.loginInfo = initialLoginInfoState;
});

export const logoutAction = () => store.setState(resetStore);
export interface LoginActionPayloadType {
address: string;
loginMethod: LoginMethodsEnum;
Expand All @@ -20,5 +14,4 @@ export const loginAction = ({ address, loginMethod }: LoginActionPayloadType) =>
account.address = address;
account.publicKey = new Address(address).hex();
loginInfo.loginMethod = loginMethod;
// setLoginExpiresAt(getNewLoginExpiresTimestamp());
});
7 changes: 7 additions & 0 deletions src/store/middleware/applyMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { StoreType } from '../store.types';
import { StoreApi } from 'zustand/vanilla';
import { logoutMiddleware } from './logoutMiddleware';

export const applyMiddleware = (store: StoreApi<StoreType>) => {
store.subscribe(logoutMiddleware);
};
1 change: 1 addition & 0 deletions src/store/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './applyMiddleware';
46 changes: 46 additions & 0 deletions src/store/middleware/logoutMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { storage } from 'storage';
import { WritableDraft } from 'immer';
import { initialState as initialAccountState } from 'store/slices/account/accountSlice';
import { initialState as initialLoginInfoState } from 'store/slices/loginInfo/loginInfoSlice';
import { localStorageKeys } from 'storage/local';
import { isLoggedInSelector } from 'store/selectors';
import { StoreType } from '../store.types';

export const resetStore = (store: WritableDraft<StoreType>) => {
store.account = initialAccountState;
store.loginInfo = initialLoginInfoState;
};

export function getNewLoginExpiresTimestamp() {
return new Date().setHours(new Date().getHours() + 24);
}

export function setLoginExpiresAt(expiresAt: number) {
storage.local.setItem({
key: localStorageKeys.loginExpiresAt,
data: expiresAt,
expires: expiresAt
});
}

export const logoutMiddleware = (newStore: StoreType) => {
const isLoggedIn = isLoggedInSelector(newStore);
const loginTimestamp = storage.local.getItem(localStorageKeys.loginExpiresAt);

if (!isLoggedIn) {
return;
}

if (loginTimestamp == null) {
setLoginExpiresAt(getNewLoginExpiresTimestamp());
return;
}

const now = Date.now();
const isExpired = loginTimestamp - now < 0;

if (isExpired) {
// logout
resetStore(newStore);
}
};
6 changes: 6 additions & 0 deletions src/store/selectors/accountSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ export const addressSelector = ({ account: { address } }: StoreType) => address;

export const accountNonceSelector = (store: StoreType) =>
accountSelector(store)?.nonce || 0;

export const isLoggedInSelector = (store: StoreType) => {
const address = addressSelector(store);
const account = accountSelector(store);
return Boolean(address && account?.address === address);
};
3 changes: 3 additions & 0 deletions src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { accountSlice } from './slices/account/accountSlice';
import { createBoundedUseStore } from './createBoundedStore';
import { loginInfoSlice } from './slices/loginInfo';
import { StoreType } from './store.types';
import { applyMiddleware } from './middleware/applyMiddleware';

export type MutatorsIn = [
['zustand/devtools', never],
Expand Down Expand Up @@ -35,6 +36,8 @@ export const store = createStore<StoreType, MutatorsOut>(
)
);

applyMiddleware(store);

export const getState = () => store.getState();

export const useStore = createBoundedUseStore(store);
Loading

0 comments on commit 327406c

Please sign in to comment.