Skip to content

Commit

Permalink
Merge pull request #1071 from jetstreamapp/chore/auth-upgrade-announc…
Browse files Browse the repository at this point in the history
…ements

Update authentication timeline
  • Loading branch information
paustint authored Nov 15, 2024
2 parents 416554d + 064dec3 commit 604f78c
Show file tree
Hide file tree
Showing 11 changed files with 144 additions and 26 deletions.
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,10 @@ package-lock.json

.nx/cache
.nx/workspace-data
vite.config.*.timestamp*
**/playwright/.auth/user.json

# Ignore data directory in scripts
/scripts/**/data/**/*
!/scripts/**/data/.gitkeep

vite.config.*.timestamp*
23 changes: 23 additions & 0 deletions apps/api/src/app/announcements.ts
Original file line number Diff line number Diff line change
@@ -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 [];
}
}
3 changes: 2 additions & 1 deletion apps/api/src/app/routes/api.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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() });
});

/**
Expand Down
7 changes: 5 additions & 2 deletions apps/jetstream/src/app/app.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -27,6 +28,7 @@ import { UnverifiedEmailAlert } from './components/core/UnverifiedEmailAlert';
export const App = () => {
const [userProfile, setUserProfile] = useState<Maybe<UserProfileUi>>();
const [featureFlags, setFeatureFlags] = useState<Set<string>>(new Set());
const [announcements, setAnnouncements] = useState<Announcement[]>([]);

useEffect(() => {
if (userProfile && userProfile[environment.authAudience || '']?.featureFlags) {
Expand All @@ -39,7 +41,7 @@ export const App = () => {
<ConfirmationServiceProvider>
<RecoilRoot>
<Suspense fallback={<AppLoading />}>
<AppInitializer onUserProfile={setUserProfile}>
<AppInitializer onAnnouncements={setAnnouncements} onUserProfile={setUserProfile}>
<OverlayProvider>
<DndProvider backend={HTML5Backend}>
<ModalContainer />
Expand All @@ -54,6 +56,7 @@ export const App = () => {
</div>
<div className="app-container slds-p-horizontal_xx-small slds-p-vertical_xx-small" data-testid="content">
<UnverifiedEmailAlert userProfile={userProfile} />
<AnnouncementAlerts announcements={announcements} />
<Suspense fallback={<AppLoading />}>
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<AppRoutes featureFlags={featureFlags} userProfile={userProfile} />
Expand Down
46 changes: 46 additions & 0 deletions apps/jetstream/src/app/components/core/AnnouncementAlerts.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<AnnouncementAlert key={announcement.id} announcement={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 (
<Alert
type="warning"
leadingIcon="warning"
allowClose
onClose={() => {
localStorage.setItem(key, 'true');
setDismissed(true);
}}
>
<span className="text-bold">{announcement.title}:</span> {announcement.content}
</Alert>
);
}
41 changes: 26 additions & 15 deletions apps/jetstream/src/app/components/core/AppInitializer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -30,13 +30,14 @@ localforage.config({
});

export interface AppInitializerProps {
onAnnouncements?: (announcements: Announcement[]) => void;
onUserProfile: (userProfile: UserProfileUi) => void;
children?: React.ReactNode;
}

export const AppInitializer: FunctionComponent<AppInitializerProps> = ({ onUserProfile, children }) => {
export const AppInitializer: FunctionComponent<AppInitializerProps> = ({ onAnnouncements, onUserProfile, children }) => {
const userProfile = useRecoilValue<UserProfileUi>(fromAppState.userProfileState);
const { version } = useRecoilValue(fromAppState.appVersionState);
const { version, announcements } = useRecoilValue(fromAppState.appVersionState);
const appCookie = useRecoilValue<ApplicationCookie>(fromAppState.applicationCookieState);
const [orgs, setOrgs] = useRecoilState(fromAppState.salesforceOrgsState);
const invalidOrg = useObservable(orgConnectionError$);
Expand All @@ -45,6 +46,10 @@ export const AppInitializer: FunctionComponent<AppInitializerProps> = ({ onUserP
console.log('APP VERSION', version);
}, [version]);

useEffect(() => {
announcements && onAnnouncements && onAnnouncements(announcements);
}, [announcements, onAnnouncements]);

useRollbar({
accessToken: environment.rollbarClientAccessToken,
environment: appCookie.environment,
Expand Down Expand Up @@ -81,20 +86,26 @@ export const AppInitializer: FunctionComponent<AppInitializerProps> = ({ 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);
Expand Down
18 changes: 16 additions & 2 deletions libs/shared/data/src/lib/client-data.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<void> {
Expand Down
8 changes: 6 additions & 2 deletions libs/shared/ui-core/src/app/HeaderNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const HeaderNavbar: FunctionComponent<HeaderNavbarProps> = ({ userProfile
? [<RecordSearchPopover />, <Jobs />, <HeaderHelpPopover />]
: [
<HeaderAnnouncementPopover>
<p className="">We are working on upgrades to our authentication and user management systems in the coming weeks.</p>
<p>We are working on upgrades to our authentication and user management systems.</p>
<p className="slds-text-title_caps slds-m-top_x-small">Upcoming Features:</p>
<ul className="slds-list_dotted slds-m-vertical_x-small">
<li>Multi-factor authentication</li>
Expand All @@ -91,9 +91,13 @@ export const HeaderNavbar: FunctionComponent<HeaderNavbarProps> = ({ userProfile
<ul className="slds-list_dotted slds-m-vertical_x-small">
<li>All users will be signed out and need to sign back in</li>
<li>Some users may require a password reset to log back in</li>
<li>Email verification will be required, and if you use a password to login, 2FA via email will be automatically enabled</li>
</ul>
<hr className="slds-m-vertical_small" />
Stay tuned for a timeline. If you have any questions <FeedbackLink type="EMAIL" label="Send us an email" />.
<p>Expected upgrade date is {new Date('2024-11-16T18:00:00.000Z').toLocaleString()}.</p>
<p>
If you have any questions <FeedbackLink type="EMAIL" label="Send us an email" />.
</p>
{!!userProfile && !userProfile.email_verified && (
<>
<hr className="slds-m-vertical_small" />
Expand Down
5 changes: 3 additions & 2 deletions libs/shared/ui-core/src/state-management/app-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -147,7 +148,7 @@ function setSelectedJetstreamOrganizationFromStorage(id: Maybe<string>) {

async function fetchAppVersion() {
try {
return isChromeExtension() ? { version: getChromeExtensionVersion() } : await checkHeartbeat();
return isChromeExtension() ? { version: getChromeExtensionVersion(), announcements: [] } : await checkHeartbeat();
} catch (ex) {
return { version: 'unknown' };
}
Expand All @@ -174,7 +175,7 @@ export const applicationCookieState = atom<ApplicationCookie>({
default: getAppCookie(),
});

export const appVersionState = atom<{ version: string }>({
export const appVersionState = atom<{ version: string; announcements?: Announcement[] }>({
key: 'appVersionState',
default: fetchAppVersion(),
});
Expand Down
9 changes: 9 additions & 0 deletions libs/types/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 604f78c

Please sign in to comment.