Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IOPID-536] : Migration from cookies to session storage #10

Merged
merged 11 commits into from
Jul 27, 2023
Merged
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1 +1 @@
BASE_ROUTE = 'Base route'
NEXT_PUBLIC_URL_SPID_LOGIN=http://localhost:9090/login
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEXT_PUBLIC_URL_SPID_LOGIN=http://localhost:9090/login
1 change: 1 addition & 0 deletions .env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEXT_PUBLIC_URL_SPID_LOGIN=http://localhost:9090/login
1 change: 1 addition & 0 deletions .env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEXT_PUBLIC_URL_SPID_LOGIN=http://localhost:9090/login
16 changes: 15 additions & 1 deletion src/app/[locale]/(pages)/access/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import { CieIcon } from '@pagopa/mui-italia/dist/icons/CieIcon';
import { SpidIcon } from '@pagopa/mui-italia/dist/icons/SpidIcon';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useState } from 'react';
import { ROUTES } from '../../_utils/routes';
import { SelectIdp } from '../../_component/selectIdp/selectIdp';
import { SpidLevels } from '../../_component/selectIdp/idpList';

const Access = (): React.ReactElement => {
const [openDialog, setOpenDialog] = useState<boolean>(false);
const t = useTranslations('access');
const spidLevel: SpidLevels = {
type: 'L2',
};

return (
<Grid container justifyContent="center" bgcolor="background.default">
Expand Down Expand Up @@ -68,7 +75,7 @@ const Access = (): React.ReactElement => {
width: '100%',
height: '50px',
}}
// onClick={() => setShowIDPS(true)}
onClick={() => setOpenDialog(true)}
variant="contained"
startIcon={<SpidIcon />}
>
Expand Down Expand Up @@ -154,6 +161,13 @@ const Access = (): React.ReactElement => {
</Link>
</Grid>
</Grid>
<SelectIdp
isOpen={openDialog}
spidLevel={spidLevel}
shadowsheep1 marked this conversation as resolved.
Show resolved Hide resolved
onClose={(opn) => {
setOpenDialog(opn);
}}
/>
</Grid>
);
};
Expand Down
13 changes: 7 additions & 6 deletions src/app/[locale]/(pages)/logoutInit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ import { Introduction } from '../../_component/introduction/introduction';
import { commonBackgroundDark } from '../../_utils/styles';
import { FAQ } from '../../_component/accordion/faqDefault';
import { SelectIdp } from '../../_component/selectIdp/selectIdp';
import { SpidLevels } from '../../_component/selectIdp/idpList';

const Init = (): React.ReactElement => {
const t = useTranslations('logout');
const [openDialog, setOpenDialog] = useState<boolean>(false);

const spidLevel: SpidLevels = {
type: 'L1',
};

return (
<>
<Grid sx={commonBackgroundDark} container>
Expand Down Expand Up @@ -64,12 +70,7 @@ const Init = (): React.ReactElement => {
</Grid>
</Grid>
<FAQ />
<SelectIdp
open={openDialog}
onClose={(opn) => {
setOpenDialog(opn);
}}
/>
<SelectIdp isOpen={openDialog} spidLevel={spidLevel} onClose={setOpenDialog} />
shadowsheep1 marked this conversation as resolved.
Show resolved Hide resolved
</>
);
};
Expand Down
19 changes: 9 additions & 10 deletions src/app/[locale]/(pages)/validateSession/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
'use client';
import { redirect } from 'next/navigation';
import { useEffect } from 'react';
import { ROUTES } from '../../_utils/routes';
import useToken from '../../_hooks/useToken';
import { redirect } from 'next/navigation';
import Loader from '../../_component/loader/loader';
import { extractToken, parseJwt, userFromJwtToken } from '../../_utils/jwt';
import { storageTokenOps, storageUserOps } from '../../_utils/storage';
import { ROUTES } from '../../_utils/routes';

const Check = (): React.ReactElement => {
const { tokenError } = useToken();

useEffect(() => {
if (tokenError === 'ERROR') {
redirect(ROUTES.LOGOUT_AUTH_KO);
}
if (tokenError === 'OK') {
if (parseJwt(extractToken())) {
// FIXME: Jira ticket number 521
storageTokenOps.write(extractToken());
storageUserOps.write(userFromJwtToken(extractToken()));
redirect(ROUTES.SESSION);
}
}, [tokenError]);
}, []);

return (
<>
Expand Down
12 changes: 0 additions & 12 deletions src/app/[locale]/_component/header/header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';
import { HeaderAccount, HeaderProduct, LogoIOApp } from '@pagopa/mui-italia';
import React from 'react';
import { IconButton } from '@mui/material';
import useLogin from '../../_hooks/useLogin';

const Header = (): React.ReactElement => {
Expand Down Expand Up @@ -37,17 +36,6 @@ const Header = (): React.ReactElement => {
}}
enableLogin={isLoggedIn}
enableAssistanceButton={true}
userActions={[
{
id: 'logout',
label: 'Esci',
onClick: () => {
// eslint-disable-next-line no-console
console.log('User logged out');
},
icon: <IconButton />,
},
]}
/>
<HeaderProduct
productsList={[
Expand Down
24 changes: 22 additions & 2 deletions src/app/[locale]/_component/selectIdp/idpList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,29 @@ import React from 'react';
import { Grid, Button, Icon } from '@mui/material';
import { IDPS, IdentityProvider } from '../../_utils/idps';

export function IdpList() {
interface IIdpList {
spidLevel: SpidLevels;
}

interface ISpidLevelL1 {
type: 'L1';
}

interface ISpidLevelL2 {
type: 'L2';
}

interface ISpidLevelL3 {
type: 'L3';
}

export type SpidLevels = ISpidLevelL1 | ISpidLevelL2 | ISpidLevelL3;

export function IdpList({ spidLevel }: IIdpList) {
const getSPID = (IDP: IdentityProvider) => {
window.location.assign(`http://localhost:9090/login?entityID=${IDP.entityId}&authLevel=SpidL1`);
window.location.assign(
`${process.env.NEXT_PUBLIC_URL_SPID_LOGIN}?entityID=${IDP.entityId}&authLevel=Spid${spidLevel.type}`
);
};

return (
Expand Down
17 changes: 11 additions & 6 deletions src/app/[locale]/_component/selectIdp/selectIdp.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { Box, Button, Dialog, Typography } from '@mui/material';
import React, { useEffect, useState } from 'react';
import { IdpList } from './idpList';
import { IdpList, SpidLevels } from './idpList';

interface IDialog {
open: boolean;
isOpen: boolean;
spidLevel: SpidLevels;
onClose: (open: boolean, event: React.MouseEvent<HTMLButtonElement>) => void;
}

export function SelectIdp({ open, onClose }: IDialog) {
export function SelectIdp({ isOpen, spidLevel, onClose }: IDialog) {
const [openDialog, setOpenDialog] = useState<boolean>(false);

useEffect(() => {
setOpenDialog(open);
}, [open]);
setOpenDialog(isOpen);
}, [isOpen]);

const level: SpidLevels = {
shadowsheep1 marked this conversation as resolved.
Show resolved Hide resolved
type: spidLevel.type,
};

return (
<>
Expand All @@ -29,7 +34,7 @@ export function SelectIdp({ open, onClose }: IDialog) {
>
{'Scegli il tuo Identity Provider'}
</Typography>
<IdpList />
<IdpList spidLevel={level} />
shadowsheep1 marked this conversation as resolved.
Show resolved Hide resolved
<Box p={4}>
<Button onClick={(e) => onClose(false, e)} fullWidth variant="outlined">
Annulla
Expand Down
57 changes: 57 additions & 0 deletions src/app/[locale]/_component/sessionProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client';

import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { LOGIN_ROUTES, PUBBLIC_ROUTES, ROUTES } from '../_utils/routes';
import useToken from '../_hooks/useToken';
import Loader from './loader/loader';
import Header from './header/header';
import Footer from './footer/footer';

type LoginStatus =
| { status: ELogin.IDLE }
| { status: ELogin.AUTHORIZED }
| { status: ELogin.NOT_AUTHORIZED };
shadowsheep1 marked this conversation as resolved.
Show resolved Hide resolved

const enum ELogin {
IDLE = 'IDLE',
AUTHORIZED = 'AUTHORIZED',
NOT_AUTHORIZED = 'NOT_AUTHORIZED',
}
shadowsheep1 marked this conversation as resolved.
Show resolved Hide resolved
const SessionProviderComponent = ({ children }: { readonly children: React.ReactNode }) => {
const [loginStatus, setLoginStatus] = useState<LoginStatus>({ status: ELogin.IDLE });
const { isTokenValid, removeToken } = useToken();
const router = useRouter();
const cleanPath = (path: string): string => path.replace(/^(\/(en|it))\/(.*)$/, '');

useEffect(() => {
if (typeof window !== 'undefined') {
if (LOGIN_ROUTES.includes(cleanPath(window.location.pathname))) {
removeToken();
}
if (PUBBLIC_ROUTES.includes(cleanPath(window.location.pathname))) {
setLoginStatus({ status: ELogin.AUTHORIZED });
}
if (!PUBBLIC_ROUTES.includes(cleanPath(window.location.pathname)) && isTokenValid()) {
setLoginStatus({ status: ELogin.AUTHORIZED });
}
if (!PUBBLIC_ROUTES.includes(cleanPath(window.location.pathname)) && !isTokenValid()) {
setLoginStatus({ status: ELogin.AUTHORIZED });
router.push(ROUTES.LOGIN);
}
}
}, [isTokenValid, removeToken, router]);

if (loginStatus.status === ELogin.IDLE || loginStatus.status === ELogin.NOT_AUTHORIZED) {
return (
<>
<Header />
<Loader />
<Footer />
</>
);
}
return <>{children}</>;
};

export default SessionProviderComponent;
9 changes: 4 additions & 5 deletions src/app/[locale]/_hooks/useLogin.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
import { User } from '../_model/User';
import { cookieTokenOps, cookieUserOps } from '../_utils/cookie';
import { storageTokenOps, storageUserOps } from '../_utils/storage';

interface LoginData {
isLoggedIn: boolean;
Expand All @@ -12,12 +12,12 @@ interface LoginData {
const useLogin = (): LoginData => {
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
const [userLogged, setUserLogged] = useState<User | undefined>(undefined);
const cookieTokenRead = cookieTokenOps.read();
const cookieTokenRead = isBrowser() ? storageTokenOps.read() : null;

useEffect(() => {
if (cookieTokenRead) {
setIsLoggedIn(true);
setUserLogged(cookieUserOps.read());
setUserLogged(storageUserOps.read());
} else {
setIsLoggedIn(false);
setUserLogged(undefined);
Expand All @@ -27,8 +27,7 @@ const useLogin = (): LoginData => {
const logOut = () => {
setIsLoggedIn(false);
setUserLogged(undefined);
cookieTokenOps.delete();
cookieUserOps.delete();
sessionStorage.clear();
};

return {
Expand Down
36 changes: 22 additions & 14 deletions src/app/[locale]/_hooks/useToken.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
'use client';
import { useEffect, useState } from 'react';
import { extractToken, parseJwt, userFromJwtToken } from '../_utils/jwt';
import { cookieTokenOps, cookieUserOps } from '../_utils/cookie';
import { storageTokenOps, storageUserOps } from '../_utils/storage';
import { isBrowser } from '../_utils/common';

interface Token {
interface IToken {
token: string;
tokenError: 'PENDING' | 'OK' | 'ERROR';
isTokenValid: () => boolean | undefined;
removeToken: () => void;
}

const useToken = (): Token => {
const useToken = (): IToken => {
const [token, setToken] = useState<string>('');
const [tokenError, setTokenError] = useState<'PENDING' | 'OK' | 'ERROR'>('PENDING');
const windowAvailable = isBrowser();

useEffect(() => {
if (parseJwt(extractToken())) {
setToken(extractToken());
setTokenError('OK');
cookieTokenOps.write(extractToken());
cookieUserOps.write(userFromJwtToken(extractToken()));
} else {
setTokenError('ERROR');
if (windowAvailable) {
setToken(storageTokenOps.read());
}
}, []);

const isTokenValid = () => {
if (windowAvailable) {
return !!storageTokenOps.read();
}
};

const removeToken = () => {
storageTokenOps.delete();
storageUserOps.delete();
};

return {
token,
tokenError,
isTokenValid,
removeToken,
};
};

Expand Down
1 change: 1 addition & 0 deletions src/app/[locale]/_utils/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isBrowser = () => typeof window !== 'undefined';
7 changes: 7 additions & 0 deletions src/app/[locale]/_utils/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ export const ROUTES = {
LOGOUT_INIT: `/logoutInit`,
SESSION: `/session`,
PROFILE: `/profile`,
PROFILE_BLOCK: `/profileBlock`,
PROFILE_RESTORE: `/profileRestore`,
RESTORE_CODE: `/restoreCode`,
RESTORE_THANK_YOU: `/restoreThankYou`,
PROFILE_BLOCK_SUCCESS: `/profileBlockSuccess`,
PROFILE_ACCESS_BLOCK: `/profileAccessBlock`,
VALIDATE_SESSION: '/validateSession',
THANK_YOU: '/thankyou',
LOGOUT_AUTH_KO: '/logoutKoAuth',
LOGOUT_KO: '/logoutKo',
KO: '/ko',
LOGOUT_NO_SESSION_L1: '/logoutNoSession/l1',
LOGOUT_NO_SESSION_L2: '/logoutNoSession/l2',
};
Expand Down
4 changes: 2 additions & 2 deletions src/app/[locale]/_utils/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import { User } from '../_model/User';
import { storageOpsBuilder } from './storage-utils';

/** An object containing a complete set of operation on the storage regarding the key used to store in the storage the loggedUser token in selfcare projects */
export const storageTokenOps = storageOpsBuilder<string>('token', 'string', true);
export const storageTokenOps = storageOpsBuilder<string>('token', 'string', false);
/** An object containing a complete set of operation on the storage regarding the key used to store in the storage the loggedUser in selfcare projects */
export const storageUserOps = storageOpsBuilder<User>('user', 'object', true);
export const storageUserOps = storageOpsBuilder<User>('user', 'object', false);
Loading