department-of-veterans-affairs/vets-website

View on GitHub
src/platform/user/authentication/downtime.js

Summary

Maintainability
A
0 mins
Test Coverage
import React from 'react';
import {
  subHours,
  isWithinInterval,
  parseJSON,
  format,
  differenceInHours,
} from 'date-fns';
import { format as fmtTZ } from 'date-fns-tz';
import { EXTERNAL_SERVICES } from 'platform/monitoring/external-services/config';
import { SERVICE_PROVIDERS } from './constants';

/* Shared */
export const AUTH_DEPENDENCIES = [
  EXTERNAL_SERVICES.idme,
  EXTERNAL_SERVICES.ssoe,
  EXTERNAL_SERVICES.dslogon,
  EXTERNAL_SERVICES.mhv,
  EXTERNAL_SERVICES.mvi,
  EXTERNAL_SERVICES.logingov,
];

export const generateCSPBanner = ({ csp }) => {
  return csp === 'idme'
    ? {
        headline: `You may have trouble signing in with some of your accounts`,
        status: 'warning',
        message: `We’re sorry. We’re working to fix some problems with ID.me. If you’d like to sign in to VA.gov with your ID.me, DS Logon, or My HealtheVet accounts, please check back later.`,
      }
    : {
        headline: `You may have trouble signing in with ${
          SERVICE_PROVIDERS[csp].label
        }`,
        status: 'warning',
        message: `We’re sorry. We’re working to fix some problems with our ${
          SERVICE_PROVIDERS[csp].label
        } sign in process. If you’d like to sign in to VA.gov with your ${
          SERVICE_PROVIDERS[csp].label
        } account, please check back later.`,
      };
};

export const DOWNTIME_BANNER_CONFIG = {
  ...Object.keys(SERVICE_PROVIDERS).reduce(
    (acc, cv) => ({
      ...acc,
      [cv]: generateCSPBanner({ csp: cv }),
    }),
    {},
  ),
  ssoe: {
    headline: 'Our sign in process isn’t working right now',
    status: 'warning',
    message:
      'We’re sorry. We’re working to fix some problems with our sign in process. If you’d like to sign in to VA.gov, please check back later.',
  },
  multipleServices: {
    headline: 'You may have trouble signing in or using some tools or services',
    status: 'warning',
    message:
      'We’re sorry. We’re working to fix a problem that affects our site. If you have trouble signing in or using any tools or services, please check back soon.',
  },
  mvi: {
    headline: 'You may have trouble signing in or using some tools or services',
    status: 'warning',
    message:
      'We’re sorry. We’re working to fix a problem that affects some parts of our site. If you have trouble signing in or using any tools or services, please check back soon.',
  },
  maintenance: {
    headline: 'Upcoming site maintenance',
    status: 'info',
  },
};

const serviceCheck = ({ serviceId, status }) =>
  AUTH_DEPENDENCIES.includes(serviceId) && status !== 'active';

/**
 *
 * @param {String} service - A singular auth dependency inside AUTH_DEPENDENCIES
 * @returns A React-node (va-alert)
 */
export const renderServiceDown = service => {
  const { status, headline, message } =
    typeof service === 'string'
      ? DOWNTIME_BANNER_CONFIG[service] ?? DOWNTIME_BANNER_CONFIG.mvi
      : service;
  return (
    <div className="form-warning-banner fed-warning--v2">
      <va-alert visible status={status} uswds>
        <h2 slot="headline">{headline}</h2>
        {message}
      </va-alert>
    </div>
  );
};

export const renderDowntimeBanner = statuses => {
  const areMultipleServicesDown = statuses?.filter(serviceCheck).length > 1;
  const downedService = areMultipleServicesDown
    ? { serviceId: 'multipleServices' }
    : statuses?.find(serviceCheck);
  return !downedService ? null : renderServiceDown(downedService.serviceId);
};

/**
 *
 * @param {Object} - A single object from a maintenance window (destructured start & end times)
 * @returns An object needed to create the React-node (va-alert) maintenance banner
 */
export const createMaintenanceBanner = ({
  start_time: startingTime,
  end_time: endingTime,
}) => {
  const { headline, status } = DOWNTIME_BANNER_CONFIG.maintenance;

  const startTime = parseJSON(startingTime);
  const startDate = format(startTime, `PPPP`);
  const endTime = parseJSON(endingTime);
  const hours = differenceInHours(endTime, startTime);
  const howLongMaintLasts = `${hours} hour${hours > 1 ? 's' : ''}`;

  const startsAt = format(startTime, `h:mm bbbb`);
  const endsAt = fmtTZ(endTime, `h:mm bbbb z`);

  const message = (
    <>
      <p>
        We’ll be working on VA.gov soon. The maintenance will last about{' '}
        {howLongMaintLasts}. During this time, you won’t be able to sign in, use
        online tools, or access VA.gov webpages.
      </p>
      <p>
        <strong>Date:</strong> {startDate}
      </p>
      <p>
        <strong>Start and end time:</strong> {startsAt} and {endsAt}
      </p>
    </>
  );
  return { headline, status, message, startTime, endTime };
};

/**
 *
 * @param {Array} maintArray - `maintenance_windows` array from API response
 * @returns An object of the first maintenance window meeting our criteria
 */
export const determineMaintenance = maintArray =>
  maintArray.find(maintService =>
    ['global', ...AUTH_DEPENDENCIES].includes(maintService.external_service),
  );

/**
 *
 * @param {ISODateString} startTime
 * @param {ISODateString} endTime
 * @returns Boolean (true/false) based on if the times are within the maintenance window
 */
export const isInMaintenanceWindow = (startTime, endTime) => {
  const start = subHours(parseJSON(startTime), 2);
  const end = parseJSON(endTime);

  const currentTime = new Date();

  return isWithinInterval(currentTime, { start, end });
};

/**
 *
 * @param {Array} maintArray - `maintenance_windows` array from API response
 * @returns A React-node (va-alert) of the maintenance window when inside a maintenance window otherwise it returns null
 */
export const renderMaintenanceWindow = maintArray => {
  if (!maintArray || maintArray.length <= 0) {
    return null;
  }

  const maintenanceBanner = createMaintenanceBanner(
    determineMaintenance(maintArray),
  );

  return maintenanceBanner &&
    isInMaintenanceWindow(
      maintenanceBanner.startTime,
      maintenanceBanner.endTime,
    )
    ? renderServiceDown(maintenanceBanner)
    : null;
};