@jpmorganchase/mosaic-site-middleware 0.1.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/README.md +14 -0
  3. package/dist/MiddlewareError.d.ts +14 -0
  4. package/dist/MiddlewareError.js +18 -0
  5. package/dist/MiddlewareError.js.map +1 -0
  6. package/dist/compileMdx.d.ts +1 -0
  7. package/dist/compileMdx.js +14 -0
  8. package/dist/compileMdx.js.map +1 -0
  9. package/dist/createMiddlewareRunner.d.ts +28 -0
  10. package/dist/createMiddlewareRunner.js +73 -0
  11. package/dist/createMiddlewareRunner.js.map +1 -0
  12. package/dist/index.d.ts +9 -0
  13. package/dist/index.js +10 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/middlewarePresets.d.ts +34 -0
  16. package/dist/middlewarePresets.js +12 -0
  17. package/dist/middlewarePresets.js.map +1 -0
  18. package/dist/plugins/codeBlocks.d.ts +22 -0
  19. package/dist/plugins/codeBlocks.js +46 -0
  20. package/dist/plugins/codeBlocks.js.map +1 -0
  21. package/dist/session.d.ts +7 -0
  22. package/dist/session.js +91 -0
  23. package/dist/session.js.map +1 -0
  24. package/dist/withContent.d.ts +17 -0
  25. package/dist/withContent.js +54 -0
  26. package/dist/withContent.js.map +1 -0
  27. package/dist/withMosaicMode.d.ts +13 -0
  28. package/dist/withMosaicMode.js +15 -0
  29. package/dist/withMosaicMode.js.map +1 -0
  30. package/dist/withSession.d.ts +21 -0
  31. package/dist/withSession.js +67 -0
  32. package/dist/withSession.js.map +1 -0
  33. package/dist/withSharedConfig.d.ts +9 -0
  34. package/dist/withSharedConfig.js +83 -0
  35. package/dist/withSharedConfig.js.map +1 -0
  36. package/dist/withStaticContent.d.ts +7 -0
  37. package/dist/withStaticContent.js +42 -0
  38. package/dist/withStaticContent.js.map +1 -0
  39. package/package.json +35 -0
  40. package/src/MiddlewareError.ts +26 -0
  41. package/src/__tests__/createMDXScope.test.ts +79 -0
  42. package/src/compileMdx.ts +19 -0
  43. package/src/createMiddlewareRunner.ts +108 -0
  44. package/src/index.ts +9 -0
  45. package/src/middlewarePresets.ts +52 -0
  46. package/src/plugins/codeBlocks.ts +52 -0
  47. package/src/session.ts +131 -0
  48. package/src/withContent.ts +67 -0
  49. package/src/withMosaicMode.ts +26 -0
  50. package/src/withSession.ts +103 -0
  51. package/src/withSharedConfig.ts +92 -0
  52. package/src/withStaticContent.ts +45 -0
  53. package/tsconfig.json +10 -0
package/src/session.ts ADDED
@@ -0,0 +1,131 @@
1
+ import cookie from 'node-cookie';
2
+ import decode from 'jwt-decode';
3
+ import { Session, JWT } from '@jpmorganchase/mosaic-types';
4
+
5
+ if (typeof window !== 'undefined') {
6
+ throw new Error('This file should not be loaded on the client.');
7
+ }
8
+
9
+ const cookieSecret = process.env.COOKIE_SECRET;
10
+ const isCookieSecretSet = !!cookieSecret;
11
+ const TOKEN_COOKIE_NAME = 'access_token';
12
+ const DEFAULT_COOKIE_CONFIG = {
13
+ httpOnly: true,
14
+ secure: process.env.NODE_ENV !== 'development',
15
+ sameSite: 'lax',
16
+ path: '/'
17
+ };
18
+
19
+ export function getToken(req): string | null {
20
+ if (!req) {
21
+ return null;
22
+ }
23
+
24
+ if (req.headers.authorization?.startsWith('Bearer ')) {
25
+ return req.headers.authorization.split(' ')[1];
26
+ }
27
+
28
+ return cookie.get(req, TOKEN_COOKIE_NAME, cookieSecret, isCookieSecretSet);
29
+ }
30
+
31
+ export function getIsCypressSession(req): boolean {
32
+ if (!req) {
33
+ return false;
34
+ }
35
+
36
+ return Boolean(req.headers?.cypresse2e === 'true');
37
+ }
38
+
39
+ export function clearSession(res) {
40
+ if (!res) {
41
+ throw new Error('Cannot clear session - response object invalid');
42
+ }
43
+ return cookie.clear(res, TOKEN_COOKIE_NAME, DEFAULT_COOKIE_CONFIG);
44
+ }
45
+
46
+ export function isSessionAgeMoreThan(session: Session, minRemainingTime = 0) {
47
+ const { expires = 0 } = session;
48
+ return expires >= Date.now() + minRemainingTime;
49
+ }
50
+
51
+ // eslint-disable-next complexity
52
+ export function getSessionFromToken(accessToken): Session | null {
53
+ try {
54
+ if (!accessToken) {
55
+ throw new Error('No token provided');
56
+ }
57
+
58
+ const decodedToken = decode(accessToken) as JWT;
59
+
60
+ if (!decodedToken) {
61
+ throw new Error('Invalid token');
62
+ }
63
+
64
+ const {
65
+ exp = -1,
66
+ UserID = '',
67
+ JPMCIdentifier = '',
68
+ UID = '',
69
+ EmployeeCorporateID = '',
70
+ CostCenter = '',
71
+ Country = '',
72
+ CountryCode = '',
73
+ Department = '',
74
+ DisplayName = '',
75
+ Email = '',
76
+ EmployeeType = '',
77
+ FirstName = '',
78
+ LastName = '',
79
+ LOB = '',
80
+ LocationCode = ''
81
+ } = decodedToken;
82
+ if (decodedToken) {
83
+ const expires = exp * 1000;
84
+
85
+ if (expires < Date.now()) {
86
+ return null;
87
+ }
88
+
89
+ const sid = UserID || JPMCIdentifier || UID || EmployeeCorporateID;
90
+ return {
91
+ expires,
92
+ isLoggedIn: true,
93
+ accessToken,
94
+ user: {
95
+ avatarUrl: process.env.AVATAR_URL!,
96
+ sid,
97
+ country: Country,
98
+ countryCode: CountryCode,
99
+ costCenter: CostCenter,
100
+ department: Department,
101
+ displayName: DisplayName,
102
+ email: Email,
103
+ employeeType: EmployeeType,
104
+ firstName: FirstName,
105
+ lastName: LastName,
106
+ lob: LOB,
107
+ locationCode: LocationCode
108
+ }
109
+ };
110
+ }
111
+ throw new Error('Could not decode token');
112
+ } catch (e) {
113
+ console.error(e);
114
+ }
115
+
116
+ return null;
117
+ }
118
+
119
+ export function createSession(res, accessToken, expires) {
120
+ return cookie.create(
121
+ res,
122
+ TOKEN_COOKIE_NAME,
123
+ accessToken,
124
+ {
125
+ ...DEFAULT_COOKIE_CONFIG,
126
+ maxAge: (expires - new Date().getTime()) / 1000
127
+ },
128
+ cookieSecret,
129
+ isCookieSecretSet
130
+ );
131
+ }
@@ -0,0 +1,67 @@
1
+ import { compileMDX } from './compileMdx.js';
2
+ import { MosaicMiddleware } from './createMiddlewareRunner.js';
3
+ import MiddlewareError from './MiddlewareError.js';
4
+
5
+ if (typeof window !== 'undefined') {
6
+ throw new Error('This file should not be loaded on the client.');
7
+ }
8
+
9
+ /**
10
+ * [[`ContentProps`]] specifies the page source/content
11
+ */
12
+ export interface ContentProps {
13
+ /** Page source, which will be page type specific */
14
+ source?: Record<string, unknown>;
15
+ /** Page type */
16
+ type?: string;
17
+ /** Raw page content, includes markdown and frontmatter */
18
+ raw?: string;
19
+ }
20
+
21
+ /**
22
+ * Adds the [[`ContentProps`]] object to the page props
23
+ * @param context
24
+ */
25
+ export const withContent: MosaicMiddleware<ContentProps> = async context => {
26
+ if (context.res.getHeader('X-Mosaic-Mode') !== 'active') {
27
+ return {};
28
+ }
29
+
30
+ const { resolvedUrl } = context;
31
+ try {
32
+ const mosaicUrl = context.res.getHeader('X-Mosaic-Content-Url');
33
+ const req = await fetch(`${mosaicUrl}${resolvedUrl}`);
34
+
35
+ if (req.ok) {
36
+ const contentType = req.headers.get('content-type');
37
+ if (contentType && contentType.includes('/mdx')) {
38
+ const text = await req.text();
39
+ const mdxSource = await compileMDX(text);
40
+ return { props: { type: 'mdx', source: mdxSource, raw: text } };
41
+ }
42
+ if (contentType && contentType.includes('/json')) {
43
+ const json = await req.json();
44
+ return { props: { type: 'json', ...json } };
45
+ }
46
+ // If redirect url was returned
47
+ } else if (req.status === 302) {
48
+ return {
49
+ redirect: {
50
+ destination: (await req.json()).redirect,
51
+ permanent: true
52
+ }
53
+ };
54
+ }
55
+ } catch (error) {
56
+ if (error instanceof Error) {
57
+ console.error(error.message);
58
+ throw new MiddlewareError(500, resolvedUrl, [error.message], { show500: true });
59
+ } else {
60
+ console.error('unexpected error');
61
+ throw new MiddlewareError(500, resolvedUrl, ['unexpected error'], { show500: true });
62
+ }
63
+ }
64
+ throw new MiddlewareError(404, resolvedUrl, [`Could not find any content for ${resolvedUrl}`], {
65
+ show404: true
66
+ });
67
+ };
@@ -0,0 +1,26 @@
1
+ import type { MosaicMode } from '@jpmorganchase/mosaic-types';
2
+
3
+ import type { MosaicMiddleware } from './createMiddlewareRunner.js';
4
+
5
+ if (typeof window !== 'undefined') {
6
+ throw new Error('This file should not be loaded on the client.');
7
+ }
8
+
9
+ /**
10
+ * [[`MosaicModeProps`]] specifies the mode mosaic uses
11
+ */
12
+ export interface MosaicModeProps {
13
+ mode?: MosaicMode;
14
+ }
15
+
16
+ /**
17
+ * Adds the [[`MosaicModeProps`]] object to the page props
18
+ * @param context
19
+ */
20
+ export const withMosaicMode: MosaicMiddleware<MosaicModeProps> = async context => {
21
+ const mode: MosaicMode = (process.env.MOSAIC_MODE || 'active') as MosaicMode;
22
+ const mosaicContentUrl = process.env[`MOSAIC_${mode.toUpperCase()}_MODE_URL`] || '';
23
+ context.res.setHeader('X-Mosaic-Mode', mode);
24
+ context.res.setHeader('X-Mosaic-Content-Url', mosaicContentUrl);
25
+ return { props: { mode } };
26
+ };
@@ -0,0 +1,103 @@
1
+ import { GetServerSidePropsContext } from 'next';
2
+ import type { Session } from '@jpmorganchase/mosaic-types';
3
+
4
+ import { getSessionFromToken, getToken, getIsCypressSession } from './session.js';
5
+ import { MosaicMiddleware } from './createMiddlewareRunner.js';
6
+ import MiddlewareError from './MiddlewareError.js';
7
+
8
+ if (typeof window !== 'undefined') {
9
+ throw new Error('This file should not be loaded on the client.');
10
+ }
11
+
12
+ /**
13
+ * [[`SessionProps`]] specifies session object containing user profile of logged in user
14
+ */
15
+ export interface SessionProps {
16
+ session?: Session;
17
+ }
18
+
19
+ /**
20
+ * [[`SessionOptions`]] specifies a configuration object for the withSession middleware
21
+ */
22
+ export interface SessionOptions {
23
+ /** is login required for the environment */
24
+ loginRequired: boolean;
25
+ }
26
+
27
+ const getRedirectOptions = (context: GetServerSidePropsContext) => {
28
+ const url = new URL(
29
+ `${process.env.HOSTNAME?.replace(/\/$/, '')}/${context.resolvedUrl.replace(/^\//, '')}`
30
+ );
31
+
32
+ if (url.search.includes('code=')) {
33
+ throw new Error(
34
+ 'Authorisation loop detected. `code` should never appear in the client query string.'
35
+ );
36
+ }
37
+ url.searchParams.delete('namespace');
38
+ url.searchParams.delete('slug');
39
+ return {
40
+ redirect: {
41
+ destination: `/api/auth/login?referrer=${encodeURIComponent(`${url.pathname}${url.search}`)}`,
42
+ permanent: false
43
+ }
44
+ };
45
+ };
46
+
47
+ async function resolveSession(accessToken) {
48
+ return (
49
+ accessToken &&
50
+ process.env.DISABLE_IDA_SERVICE_AUTH_TOKEN_VERIFICATION === 'true' &&
51
+ (getSessionFromToken(accessToken) as Session)
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Adds the [[`Session`]] object to the page props
57
+ * @param context
58
+ * @param options
59
+ */
60
+ export const withSession: MosaicMiddleware<SessionProps, SessionOptions> = async (
61
+ context,
62
+ options
63
+ ) => {
64
+ if (process.env.ENABLE_LOGIN !== 'true') {
65
+ return {};
66
+ }
67
+ if (!process.env.COOKIE_SECRET) {
68
+ const errorMessage = '`process.env.COOKIE_SECRET` must be set in environment variables.';
69
+ throw new MiddlewareError(500, context.resolvedUrl, [errorMessage], {
70
+ show500: true
71
+ });
72
+ }
73
+ const accessToken = getToken(context.req);
74
+ const isCypressSession = getIsCypressSession(context.req);
75
+ const session = await resolveSession(accessToken);
76
+ // Do the middleware options have `loginRequired` true?
77
+ const isRedirectEnabled = options?.loginRequired;
78
+ const isLoggedIn = !!(
79
+ accessToken &&
80
+ session &&
81
+ // If session remaining time is <15m we'll request a re-login
82
+ isSessionRemainingMoreThan(session, 900000 /* 15 mins */)
83
+ );
84
+ if (isRedirectEnabled && !isLoggedIn) {
85
+ return getRedirectOptions(context);
86
+ }
87
+
88
+ return {
89
+ props: {
90
+ session: {
91
+ ...session,
92
+ isLoggedIn,
93
+ accessToken,
94
+ isCypressSession
95
+ }
96
+ }
97
+ };
98
+ };
99
+
100
+ function isSessionRemainingMoreThan(session: Session, minRemainingTime = 1800000 /* 30 mins */) {
101
+ const { expires = 0 } = session;
102
+ return expires >= Date.now() + minRemainingTime;
103
+ }
@@ -0,0 +1,92 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { GetServerSidePropsContext } from 'next';
4
+ import type { SharedConfig, SharedConfigSlice } from '@jpmorganchase/mosaic-store';
5
+ import { MosaicMiddleware } from './createMiddlewareRunner.js';
6
+ import MiddlewareError from './MiddlewareError.js';
7
+
8
+ if (typeof window !== 'undefined') {
9
+ throw new Error('This file should not be loaded on the client.');
10
+ }
11
+
12
+ export { SharedConfig };
13
+
14
+ /**
15
+ * Fetch from mosaic the shared-config.json.
16
+ *
17
+ * @param url the shared config file URL
18
+ * @returns shared config JSON
19
+ */
20
+ async function fetchSharedConfig(url: string) {
21
+ const response = await fetch(url, {
22
+ headers: {
23
+ 'Content-Type': 'application/json'
24
+ }
25
+ });
26
+ return response;
27
+ }
28
+
29
+ /**
30
+ * Read from the file system the shared config
31
+ *
32
+ * @param url the shared config file URL
33
+ * @returns shared config JSON
34
+ */
35
+ async function readSharedConfig(url: string) {
36
+ const mosaicSnapshotDir = process.env.MOSAIC_SNAPSHOT_DIR || '';
37
+ const filePath = path.posix.join(process.cwd(), mosaicSnapshotDir, url);
38
+ try {
39
+ const realPath = await fs.promises.realpath(filePath);
40
+ const data = await fs.promises.readFile(realPath, 'utf-8');
41
+ return new Response(data.toString(), { status: 200 });
42
+ } catch (e) {
43
+ // it doesn't matter if no shared config file was found
44
+ return new Response('', { status: 204 });
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Adds the [[`SharedConfig`]] props to the page props
50
+ * @param _context
51
+ * @param _params
52
+ */
53
+ export const withSharedConfig: MosaicMiddleware<SharedConfigSlice> = async (
54
+ context: GetServerSidePropsContext
55
+ ) => {
56
+ const { resolvedUrl, res } = context;
57
+ const isStatic = res.getHeader('X-Mosaic-Mode') === 'static';
58
+ const matches = resolvedUrl.match(/(.*)[!/]/);
59
+ const urlPath = matches?.length ? matches[1] : '';
60
+ const mosaicUrl = isStatic ? '' : res.getHeader('X-Mosaic-Content-Url');
61
+ const sharedConfigUrl = `${mosaicUrl}${urlPath}/shared-config.json`;
62
+ let response;
63
+ try {
64
+ response = isStatic
65
+ ? await readSharedConfig(sharedConfigUrl)
66
+ : await fetchSharedConfig(sharedConfigUrl);
67
+
68
+ if (response.ok && response.status !== 204) {
69
+ const { config } = (await response.json()) as { config: SharedConfig };
70
+
71
+ return { props: { sharedConfig: config } };
72
+ }
73
+ } catch (error) {
74
+ if (error instanceof Error) {
75
+ console.error(error.message);
76
+ throw new MiddlewareError(500, sharedConfigUrl, [error.message], { show500: true });
77
+ } else {
78
+ console.error('unexpected error');
79
+ throw new MiddlewareError(500, sharedConfigUrl, ['unexpected error'], { show500: true });
80
+ }
81
+ }
82
+ const show500 = response.status !== 404 && response.status !== 204;
83
+ const show404 = response.status === 404;
84
+ let errorMessage = `Could not find any shared config defined for ${sharedConfigUrl}`;
85
+ if (show500) {
86
+ errorMessage = `An un-expected error occurred reading ${sharedConfigUrl}`;
87
+ }
88
+ throw new MiddlewareError(response.status, sharedConfigUrl, [errorMessage], {
89
+ show404,
90
+ show500
91
+ });
92
+ };
@@ -0,0 +1,45 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+
4
+ import { compileMDX } from './compileMdx.js';
5
+ import { MosaicMiddleware } from './createMiddlewareRunner.js';
6
+ import MiddlewareError from './MiddlewareError.js';
7
+ import { ContentProps } from './withContent.js';
8
+
9
+ /**
10
+ * Adds the [[`ContentProps`]] object to the page props
11
+ * @param context
12
+ */
13
+ export const withStaticContent: MosaicMiddleware<ContentProps> = async context => {
14
+ const { resolvedUrl, res } = context;
15
+ const isStatic = res.getHeader('X-Mosaic-Mode') === 'static';
16
+
17
+ if (!isStatic) {
18
+ return {};
19
+ }
20
+ const mosaicSnapshotDir = process.env.MOSAIC_SNAPSHOT_DIR || '';
21
+ // Find the absolute path for the file/dir requested
22
+ const filePath = path.posix.join(process.cwd(), mosaicSnapshotDir, resolvedUrl);
23
+
24
+ try {
25
+ const realPath = await fs.promises.realpath(filePath);
26
+ const extname = path.extname(realPath);
27
+ if (extname === '.mdx') {
28
+ const data = await fs.promises.readFile(filePath, 'utf-8');
29
+ const text = data.toString();
30
+ const mdxSource = await compileMDX(text);
31
+ return { props: { type: 'mdx', source: mdxSource, raw: text } };
32
+ }
33
+ } catch (error) {
34
+ if (error instanceof Error) {
35
+ console.error(error.message);
36
+ throw new MiddlewareError(500, resolvedUrl, [error.message], { show500: true });
37
+ } else {
38
+ console.error('unexpected error');
39
+ throw new MiddlewareError(500, resolvedUrl, ['unexpected error'], { show500: true });
40
+ }
41
+ }
42
+ throw new MiddlewareError(404, resolvedUrl, [`Could not find any content for ${resolvedUrl}`], {
43
+ show404: true
44
+ });
45
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.bundle.json",
3
+ "compilerOptions": {
4
+ "emitDeclarationOnly": false,
5
+ "rootDir": "./src",
6
+ "outDir": "./dist"
7
+ },
8
+ "include": ["src"],
9
+ "exclude": ["src/**/__tests__/**/*", "src/__tests__/**/*"]
10
+ }