From 064dec33218ffd21fdae81c7c5ce5dc218e6ad86 Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Fri, 15 Nov 2024 08:57:05 -0700 Subject: [PATCH] Update authentication timeline Add downtime announcement and update verbiage for authentication --- .gitignore | 8 +++- apps/api/src/app/announcements.ts | 23 ++++++++++ apps/api/src/app/routes/api.routes.ts | 3 +- apps/jetstream/src/app/app.tsx | 7 ++- .../components/core/AnnouncementAlerts.tsx | 46 +++++++++++++++++++ .../app/components/core/AppInitializer.tsx | 41 +++++++++++------ libs/shared/data/src/lib/client-data.ts | 18 +++++++- libs/shared/ui-core/src/app/HeaderNavbar.tsx | 8 +++- .../ui-core/src/state-management/app-state.ts | 5 +- libs/types/src/lib/types.ts | 9 ++++ package.json | 2 +- 11 files changed, 144 insertions(+), 26 deletions(-) create mode 100644 apps/api/src/app/announcements.ts create mode 100644 apps/jetstream/src/app/components/core/AnnouncementAlerts.tsx diff --git a/.gitignore b/.gitignore index 5cd6d0e6..42dc525a 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,10 @@ package-lock.json .nx/cache .nx/workspace-data -vite.config.*.timestamp* \ No newline at end of file +**/playwright/.auth/user.json + +# Ignore data directory in scripts +/scripts/**/data/**/* +!/scripts/**/data/.gitkeep + +vite.config.*.timestamp* diff --git a/apps/api/src/app/announcements.ts b/apps/api/src/app/announcements.ts new file mode 100644 index 00000000..6dd0c8ea --- /dev/null +++ b/apps/api/src/app/announcements.ts @@ -0,0 +1,23 @@ +import { logger } from '@jetstream/api-config'; +import { getErrorMessageAndStackObj } from '@jetstream/shared/utils'; +import { Announcement } from '@jetstream/types'; + +export function getAnnouncements(): Announcement[] { + try { + // This is a placeholder for the announcements that will be stored in the database eventually + return [ + { + id: 'auth-downtime-2024-11-15T15:00:00.000Z', + title: 'Downtime', + content: + 'We will be upgrading our authentication system with an expected start time of {start} in your local timezone. During this time, you will not be able to log in or use Jetstream. We expect the upgrade to take less than one hour.', + replacementDates: [{ key: '{start}', value: '2024-11-16T18:00:00.000Z' }], + expiresAt: '2024-11-16T20:00:00.000Z', + createdAt: '2024-11-15T15:00:00.000Z', + }, + ].filter(({ expiresAt }) => new Date(expiresAt) > new Date()); + } catch (ex) { + logger.error({ ...getErrorMessageAndStackObj(ex) }, 'Failed to get announcements'); + return []; + } +} diff --git a/apps/api/src/app/routes/api.routes.ts b/apps/api/src/app/routes/api.routes.ts index 25e54fd5..50e27bed 100644 --- a/apps/api/src/app/routes/api.routes.ts +++ b/apps/api/src/app/routes/api.routes.ts @@ -2,6 +2,7 @@ import { ENV } from '@jetstream/api-config'; import express from 'express'; import Router from 'express-promise-router'; import multer from 'multer'; +import { getAnnouncements } from '../announcements'; import { routeDefinition as imageController } from '../controllers/image.controller'; import { routeDefinition as jetstreamOrganizationsController } from '../controllers/jetstream-organizations.controller'; import { routeDefinition as orgsController } from '../controllers/orgs.controller'; @@ -26,7 +27,7 @@ routes.use(addOrgsToLocal); // used to make sure the user is authenticated and can communicate with the server routes.get('/heartbeat', (req: express.Request, res: express.Response) => { - sendJson(res, { version: ENV.GIT_VERSION || null }); + sendJson(res, { version: ENV.GIT_VERSION || null, announcements: getAnnouncements() }); }); /** diff --git a/apps/jetstream/src/app/app.tsx b/apps/jetstream/src/app/app.tsx index ed93065f..40812588 100644 --- a/apps/jetstream/src/app/app.tsx +++ b/apps/jetstream/src/app/app.tsx @@ -1,4 +1,4 @@ -import { Maybe, UserProfileUi } from '@jetstream/types'; +import { Announcement, Maybe, UserProfileUi } from '@jetstream/types'; import { AppToast, ConfirmationServiceProvider } from '@jetstream/ui'; // import { initSocket } from '@jetstream/shared/data'; import { AppLoading, DownloadFileStream, ErrorBoundaryFallback, HeaderNavbar } from '@jetstream/ui-core'; @@ -11,6 +11,7 @@ import ModalContainer from 'react-modal-promise'; import { RecoilRoot } from 'recoil'; import { environment } from '../environments/environment'; import { AppRoutes } from './AppRoutes'; +import { AnnouncementAlerts } from './components/core/AnnouncementAlerts'; import AppInitializer from './components/core/AppInitializer'; import AppStateResetOnOrgChange from './components/core/AppStateResetOnOrgChange'; import LogInitializer from './components/core/LogInitializer'; @@ -27,6 +28,7 @@ import { UnverifiedEmailAlert } from './components/core/UnverifiedEmailAlert'; export const App = () => { const [userProfile, setUserProfile] = useState>(); const [featureFlags, setFeatureFlags] = useState>(new Set()); + const [announcements, setAnnouncements] = useState([]); useEffect(() => { if (userProfile && userProfile[environment.authAudience || '']?.featureFlags) { @@ -39,7 +41,7 @@ export const App = () => { }> - + @@ -54,6 +56,7 @@ export const App = () => {
+ }> diff --git a/apps/jetstream/src/app/components/core/AnnouncementAlerts.tsx b/apps/jetstream/src/app/components/core/AnnouncementAlerts.tsx new file mode 100644 index 00000000..e9323b8f --- /dev/null +++ b/apps/jetstream/src/app/components/core/AnnouncementAlerts.tsx @@ -0,0 +1,46 @@ +import { Announcement } from '@jetstream/types'; +import { Alert } from '@jetstream/ui'; +import { useState } from 'react'; + +interface UnverifiedEmailAlertProps { + announcements: Announcement[]; +} + +const LS_KEY_PREFIX = 'announcement_dismissed_'; + +export function AnnouncementAlerts({ announcements }: UnverifiedEmailAlertProps) { + if (!announcements || !announcements.length) { + return null; + } + + return ( + <> + {announcements.map((announcement) => ( + + ))} + + ); +} + +export function AnnouncementAlert({ announcement }: { announcement: Announcement }) { + const key = `${LS_KEY_PREFIX}${announcement.id}`; + const [dismissed, setDismissed] = useState(() => localStorage.getItem(key) === 'true'); + + if (dismissed || !announcement || !announcement.id || !announcement.content) { + return null; + } + + return ( + { + localStorage.setItem(key, 'true'); + setDismissed(true); + }} + > + {announcement.title}: {announcement.content} + + ); +} diff --git a/apps/jetstream/src/app/components/core/AppInitializer.tsx b/apps/jetstream/src/app/components/core/AppInitializer.tsx index c605a38b..dd9ecb4a 100644 --- a/apps/jetstream/src/app/components/core/AppInitializer.tsx +++ b/apps/jetstream/src/app/components/core/AppInitializer.tsx @@ -3,7 +3,7 @@ import { logger } from '@jetstream/shared/client-logger'; import { HTTP } from '@jetstream/shared/constants'; import { checkHeartbeat, registerMiddleware } from '@jetstream/shared/data'; import { useObservable, useRollbar } from '@jetstream/shared/ui-utils'; -import { ApplicationCookie, SalesforceOrgUi, UserProfileUi } from '@jetstream/types'; +import { Announcement, ApplicationCookie, SalesforceOrgUi, UserProfileUi } from '@jetstream/types'; import { fromAppState, useAmplitude, usePageViews } from '@jetstream/ui-core'; import { AxiosResponse } from 'axios'; import localforage from 'localforage'; @@ -30,13 +30,14 @@ localforage.config({ }); export interface AppInitializerProps { + onAnnouncements?: (announcements: Announcement[]) => void; onUserProfile: (userProfile: UserProfileUi) => void; children?: React.ReactNode; } -export const AppInitializer: FunctionComponent = ({ onUserProfile, children }) => { +export const AppInitializer: FunctionComponent = ({ onAnnouncements, onUserProfile, children }) => { const userProfile = useRecoilValue(fromAppState.userProfileState); - const { version } = useRecoilValue(fromAppState.appVersionState); + const { version, announcements } = useRecoilValue(fromAppState.appVersionState); const appCookie = useRecoilValue(fromAppState.applicationCookieState); const [orgs, setOrgs] = useRecoilState(fromAppState.salesforceOrgsState); const invalidOrg = useObservable(orgConnectionError$); @@ -45,6 +46,10 @@ export const AppInitializer: FunctionComponent = ({ onUserP console.log('APP VERSION', version); }, [version]); + useEffect(() => { + announcements && onAnnouncements && onAnnouncements(announcements); + }, [announcements, onAnnouncements]); + useRollbar({ accessToken: environment.rollbarClientAccessToken, environment: appCookie.environment, @@ -81,20 +86,26 @@ export const AppInitializer: FunctionComponent = ({ onUserP * 1. ensure user is still authenticated * 2. make sure the app version has not changed, if it has then refresh the page */ - const handleWindowFocus = useCallback(async (event: FocusEvent) => { - try { - if (document.visibilityState === 'visible') { - const { version: serverVersion } = await checkHeartbeat(); - // TODO: inform user that there is a new version and that they should refresh their browser. - // We could force refresh, but don't want to get into some weird infinite refresh state - if (version !== serverVersion) { - console.log('VERSION MISMATCH', { serverVersion, version }); + const handleWindowFocus = useCallback( + async (event: FocusEvent) => { + try { + if (document.visibilityState === 'visible') { + const { version: serverVersion, announcements } = await checkHeartbeat(); + // TODO: inform user that there is a new version and that they should refresh their browser. + // We could force refresh, but don't want to get into some weird infinite refresh state + if (version !== serverVersion) { + console.log('VERSION MISMATCH', { serverVersion, version }); + } + if (announcements && onAnnouncements) { + onAnnouncements(announcements); + } } + } catch (ex) { + // ignore error, but user should have been logged out if this failed } - } catch (ex) { - // ignore error, but user should have been logged out if this failed - } - }, []); + }, + [onAnnouncements, version] + ); useEffect(() => { document.addEventListener('visibilitychange', handleWindowFocus); diff --git a/libs/shared/data/src/lib/client-data.ts b/libs/shared/data/src/lib/client-data.ts index 55a2312d..fcf655ba 100644 --- a/libs/shared/data/src/lib/client-data.ts +++ b/libs/shared/data/src/lib/client-data.ts @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { logger } from '@jetstream/shared/client-logger'; import { HTTP, MIME_TYPES } from '@jetstream/shared/constants'; import { + Announcement, AnonymousApexResponse, ApexCompletionResponse, ApiResponse, @@ -64,8 +66,20 @@ function convertDateToLocale(dateOrIsoDateString?: string | Date, options?: Intl //// APPLICATION ROUTES -export async function checkHeartbeat(): Promise<{ version: string }> { - return handleRequest({ method: 'GET', url: '/api/heartbeat' }).then(unwrapResponseIgnoreCache); +export async function checkHeartbeat(): Promise<{ version: string; announcements?: Announcement[] }> { + const heartbeat = await handleRequest<{ version: string; announcements?: Announcement[] }>({ method: 'GET', url: '/api/heartbeat' }).then( + unwrapResponseIgnoreCache + ); + try { + heartbeat?.announcements?.forEach((item) => { + item?.replacementDates?.forEach(({ key, value }) => { + item.content = item.content.replaceAll(key, new Date(value).toLocaleString()); + }); + }); + } catch (ex) { + logger.warn('Unable to parse announcements'); + } + return heartbeat; } export async function emailSupport(emailBody: string, attachments: InputReadFileContent[]): Promise { diff --git a/libs/shared/ui-core/src/app/HeaderNavbar.tsx b/libs/shared/ui-core/src/app/HeaderNavbar.tsx index fb991815..1933fc88 100644 --- a/libs/shared/ui-core/src/app/HeaderNavbar.tsx +++ b/libs/shared/ui-core/src/app/HeaderNavbar.tsx @@ -81,7 +81,7 @@ export const HeaderNavbar: FunctionComponent = ({ userProfile ? [, , ] : [ -

We are working on upgrades to our authentication and user management systems in the coming weeks.

+

We are working on upgrades to our authentication and user management systems.

Upcoming Features:

  • Multi-factor authentication
  • @@ -91,9 +91,13 @@ export const HeaderNavbar: FunctionComponent = ({ userProfile
    • All users will be signed out and need to sign back in
    • Some users may require a password reset to log back in
    • +
    • Email verification will be required, and if you use a password to login, 2FA via email will be automatically enabled

    - Stay tuned for a timeline. If you have any questions . +

    Expected upgrade date is {new Date('2024-11-16T18:00:00.000Z').toLocaleString()}.

    +

    + If you have any questions . +

    {!!userProfile && !userProfile.email_verified && ( <>
    diff --git a/libs/shared/ui-core/src/state-management/app-state.ts b/libs/shared/ui-core/src/state-management/app-state.ts index c1c3364c..44d036f9 100644 --- a/libs/shared/ui-core/src/state-management/app-state.ts +++ b/libs/shared/ui-core/src/state-management/app-state.ts @@ -5,6 +5,7 @@ import { checkHeartbeat, getJetstreamOrganizations, getOrgs, getUserProfile } fr import { getChromeExtensionVersion, getOrgType, isChromeExtension, parseCookie } from '@jetstream/shared/ui-utils'; import { groupByFlat, orderObjectsBy } from '@jetstream/shared/utils'; import { + Announcement, ApplicationCookie, JetstreamOrganization, JetstreamOrganizationWithOrgs, @@ -147,7 +148,7 @@ function setSelectedJetstreamOrganizationFromStorage(id: Maybe) { async function fetchAppVersion() { try { - return isChromeExtension() ? { version: getChromeExtensionVersion() } : await checkHeartbeat(); + return isChromeExtension() ? { version: getChromeExtensionVersion(), announcements: [] } : await checkHeartbeat(); } catch (ex) { return { version: 'unknown' }; } @@ -174,7 +175,7 @@ export const applicationCookieState = atom({ default: getAppCookie(), }); -export const appVersionState = atom<{ version: string }>({ +export const appVersionState = atom<{ version: string; announcements?: Announcement[] }>({ key: 'appVersionState', default: fetchAppVersion(), }); diff --git a/libs/types/src/lib/types.ts b/libs/types/src/lib/types.ts index c00f039f..d3031a5b 100644 --- a/libs/types/src/lib/types.ts +++ b/libs/types/src/lib/types.ts @@ -3,6 +3,15 @@ import { SalesforceOrgEdition } from './salesforce/misc.types'; import { QueryResult } from './salesforce/query.types'; import { InsertUpdateUpsertDeleteQuery } from './salesforce/record.types'; +export interface Announcement { + id: string; + title: string; + content: string; + replacementDates: { key: string; value: string }[]; + expiresAt: string; + createdAt: string; +} + export type CopyAsDataType = 'excel' | 'csv' | 'json'; export interface RequestResult { diff --git a/package.json b/package.json index 53dd1a70..9a2b5e70 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "build:web-extension:dev": "nx run jetstream-web-extension:build --configuration=development", "build:web-extension": "nx run jetstream-web-extension:build", "scripts:replace-deps": "node ./scripts/replace-package-deps.mjs", - "release": "dotenv -- release-it -V ${0}", + "release": "dotenv -- release-it -V", "release:build": "zx ./scripts/build-release.mjs", "release:web-extension": "dotenv -- release-it --config .release-it-web-ext.json", "bundle-analyzer:client": "npx webpack-bundle-analyzer dist/apps/jetstream/stats.json",