teamdigitale/italia-app

View on GitHub
ts/store/reducers/authentication.ts

Summary

Maintainability
C
1 day
Test Coverage
import * as O from "fp-ts/lib/Option";
import { PersistPartial } from "redux-persist";
import { createSelector } from "reselect";
import { isActionOf } from "typesafe-actions";
import { PublicSession } from "../../../definitions/session_manager/PublicSession";
import { SessionToken } from "../../types/SessionToken";
import {
  idpSelected,
  loginSuccess,
  logoutFailure,
  logoutSuccess,
  resetAuthenticationState,
  sessionExpired,
  sessionInformationLoadSuccess,
  sessionInvalid
} from "../actions/authentication";
import { Action } from "../actions/types";
import { SpidIdp } from "../../../definitions/content/SpidIdp";
import { refreshSessionToken } from "../../features/fastLogin/store/actions/tokenRefreshActions";
import { logoutRequest } from "./../actions/authentication";
import { GlobalState } from "./types";

// Types

// reason for the user to be in the unauthenticated state
type LoggedOutReason = "NOT_LOGGED_IN" | "SESSION_EXPIRED";

// PublicSession attributes
export type TokenName = keyof Omit<
  PublicSession,
  "spidLevel" | "lollipopAssertionRef"
>;

// The user is logged out and hasn't selected an IDP
type LoggedOutWithoutIdp = Readonly<{
  kind: "LoggedOutWithoutIdp";
  reason: LoggedOutReason;
}>;

// The user is logged out but has already selected an IDP
export type LoggedOutWithIdp = Readonly<{
  kind: "LoggedOutWithIdp";
  idp: SpidIdp;
  reason: LoggedOutReason;
}>;

// The user is logged in but we still have to request the addition session info to the Backend
export type LoggedInWithoutSessionInfo = Readonly<{
  kind: "LoggedInWithoutSessionInfo";
  idp: SpidIdp;
  sessionToken: SessionToken;
}>;

// The user is logged in and we also have all session info
export type LoggedInWithSessionInfo = Readonly<{
  kind: "LoggedInWithSessionInfo";
  idp: SpidIdp;
  sessionToken: SessionToken;
  sessionInfo: PublicSession;
}>;

export type LogoutRequested = Readonly<{
  kind: "LogoutRequested";
  idp: SpidIdp;
  reason: LoggedOutReason;
}>;

export type AuthenticationState =
  | LoggedOutWithoutIdp
  | LoggedOutWithIdp
  | LogoutRequested
  | LoggedInWithoutSessionInfo
  | LoggedInWithSessionInfo;

type AuthenticationStateWithIdp =
  | LoggedOutWithIdp
  | LogoutRequested
  | LoggedInWithoutSessionInfo
  | LoggedInWithSessionInfo;

// Here we mix the plain AuthenticationState with the keys added by redux-persist
export type PersistedAuthenticationState = AuthenticationState & PersistPartial;

// Initially the user is logged out and hasn't selected an IDP
export const INITIAL_STATE: LoggedOutWithoutIdp = {
  kind: "LoggedOutWithoutIdp",
  reason: "NOT_LOGGED_IN"
};

// Type guards

export function isLoggedOutWithIdp(
  state: AuthenticationState
): state is LoggedOutWithIdp {
  return state.kind === "LoggedOutWithIdp";
}

function isLoggedInWithoutSessionInfo(
  state: AuthenticationState
): state is LoggedInWithoutSessionInfo {
  return state.kind === "LoggedInWithoutSessionInfo";
}

export function isLoggedInWithSessionInfo(
  state: AuthenticationState
): state is LoggedInWithSessionInfo {
  return state.kind === "LoggedInWithSessionInfo";
}

export function isLoggedIn(
  state: AuthenticationState
): state is LoggedInWithoutSessionInfo | LoggedInWithSessionInfo {
  return (
    isLoggedInWithoutSessionInfo(state) || isLoggedInWithSessionInfo(state)
  );
}

export function isSessionExpired(
  state: AuthenticationState
): state is LoggedOutWithoutIdp | LoggedOutWithIdp {
  return isLoggedOutWithIdp(state) && state.reason === "SESSION_EXPIRED";
}

// Selectors
export const authenticationStateSelector = (
  state: GlobalState
): AuthenticationState => state.authentication;

export const loggedOutWithIdpAuthSelector = createSelector(
  authenticationStateSelector,
  authState => (isLoggedOutWithIdp(authState) ? authState : undefined)
);

export const isLogoutRequested = (state: GlobalState) =>
  state.authentication.kind === "LogoutRequested";

export const isSessionExpiredSelector = (state: GlobalState) =>
  !isLoggedIn(state.authentication) && isSessionExpired(state.authentication);

export const sessionTokenSelector = (
  state: GlobalState
): SessionToken | undefined =>
  isLoggedIn(state.authentication)
    ? state.authentication.sessionToken
    : undefined;

export const fimsTokenSelector = (state: GlobalState): string | undefined =>
  isLoggedInWithSessionInfo(state.authentication)
    ? state.authentication.sessionInfo.fimsToken
    : undefined;

/**
 * Return the authentication header required for IO Backend requests
 */
export const ioBackendAuthenticationHeaderSelector = createSelector(
  sessionTokenSelector,
  (token): { [key: string]: string } => ({ Authorization: `Bearer ${token}` })
);

export const sessionInfoSelector = createSelector(
  (state: GlobalState) => state.authentication,
  authentication =>
    isLoggedInWithSessionInfo(authentication)
      ? O.some(authentication.sessionInfo)
      : O.none
);

export const zendeskTokenSelector = (state: GlobalState): string | undefined =>
  isLoggedInWithSessionInfo(state.authentication)
    ? state.authentication.sessionInfo.zendeskToken
    : undefined;

export const walletTokenSelector = (state: GlobalState): string | undefined =>
  isLoggedInWithSessionInfo(state.authentication)
    ? state.authentication.sessionInfo.walletToken
    : undefined;

export const bpdTokenSelector = (state: GlobalState): string | undefined =>
  isLoggedInWithSessionInfo(state.authentication)
    ? state.authentication.sessionInfo.bpdToken
    : undefined;

export const loggedInIdpSelector = (state: GlobalState) =>
  isLoggedIn(state.authentication) ? state.authentication.idp : undefined;

export const isLoggedInWithTestIdpSelector = (state: GlobalState) =>
  isLoggedIn(state.authentication) && state.authentication.idp.isTestIdp;

export const selectedIdentityProviderSelector = createSelector(
  authenticationStateSelector,
  authState => (isLoggedOutWithIdp(authState) ? authState.idp : undefined)
);

function matchWithIdp<I>(
  state: AuthenticationState,
  whenWithoutIdp: I,
  whenWithIdp: (state: AuthenticationStateWithIdp) => I
): I {
  if (state.kind === "LoggedOutWithoutIdp") {
    return whenWithoutIdp;
  }

  return whenWithIdp(state);
}

export const idpSelector = ({
  authentication
}: GlobalState): O.Option<SpidIdp> =>
  matchWithIdp(authentication, O.none, ({ idp }) => O.some(idp));

export const loggedInAuthSelector = ({ authentication }: GlobalState) =>
  isLoggedIn(authentication) ? authentication : undefined;

const reducer = (
  state: AuthenticationState = INITIAL_STATE,
  action: Action
  // eslint-disable-next-line sonarjs/cognitive-complexity
): AuthenticationState => {
  if (isActionOf(idpSelected, action) && !isLoggedIn(state)) {
    // Save the selected IDP in the state
    return {
      ...state,
      ...{
        kind: "LoggedOutWithIdp",
        idp: action.payload
      }
    };
  }

  if (isActionOf(loginSuccess, action) && isLoggedOutWithIdp(state)) {
    // Save the SessionToken (got from the WebView redirect url) in the state
    return {
      kind: "LoggedInWithoutSessionInfo",
      idp: state.idp,
      sessionToken: action.payload.token
    };
  }

  if (isActionOf(refreshSessionToken.success, action) && isLoggedIn(state)) {
    // Save the new SessionToken in the state
    return {
      ...state,
      ...{
        sessionToken: action.payload
      }
    };
  }

  if (isActionOf(sessionInformationLoadSuccess, action) && isLoggedIn(state)) {
    // Save the session info in the state
    return {
      ...state,
      ...{
        kind: "LoggedInWithSessionInfo",
        sessionInfo: action.payload
      }
    };
  }

  if (isActionOf(logoutRequest, action) && isLoggedIn(state)) {
    return {
      ...state,
      ...{
        kind: "LogoutRequested",
        reason: "NOT_LOGGED_IN"
      }
    };
  }

  if (
    (isActionOf(sessionExpired, action) ||
      isActionOf(sessionInvalid, action) ||
      isActionOf(logoutSuccess, action) ||
      isActionOf(logoutFailure, action)) &&
    isLoggedIn(state)
  ) {
    return {
      kind: "LoggedOutWithIdp",
      idp: state.idp,
      reason: isActionOf(sessionExpired, action)
        ? "SESSION_EXPIRED"
        : "NOT_LOGGED_IN"
    };
  }

  if (isActionOf(resetAuthenticationState, action) && isSessionExpired(state)) {
    return INITIAL_STATE;
  }

  return state;
};

export default reducer;