Skip to content

Commit

Permalink
FaustNX Auth Utilities (wpengine#947)
Browse files Browse the repository at this point in the history
* Setup config getter/setter

* Add proper auth functionality to FaustNX

* Add tests / folder structure

* Delete coverage

* Fix tests for FaustNX

* Delete coverage files

* Add back `.gitignore`

* Add exports to `faust-nx`

* Add faustnx scripts to monorepo root

* Update docs site package lock

* Downgrade `@ypes/react`

* Add faustnx workspace to clean script

Co-authored-by: Joseph Fusco <[email protected]>

* Add clean script to FaustNX

* Add FaustNX codecoverage check

Signed-off-by: Joe Fusco <[email protected]>

* `skipLibCheck` in TS Config

* Add jest npm scripts

Signed-off-by: Joe Fusco <[email protected]>

* Add CJS/MJS builds for FaustNX

* Fix package.json

* Clean before build & sort scripts A->Z

Signed-off-by: Joe Fusco <[email protected]>

* Revert "Add FaustNX codecoverage check"

This reverts commit 972283f.

Signed-off-by: Joe Fusco <[email protected]>

* Fix CJS/MJS dist builds

Co-authored-by: Joseph Fusco <[email protected]>
Co-authored-by: Joe Fusco <[email protected]>
  • Loading branch information
3 people authored Jul 29, 2022
1 parent a585eff commit 148a1dc
Show file tree
Hide file tree
Showing 39 changed files with 8,708 additions and 4,058 deletions.
603 changes: 348 additions & 255 deletions internal/website/package-lock.json

Large diffs are not rendered by default.

10,487 changes: 6,711 additions & 3,776 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
]
},
"scripts": {
"build": "npm run build --workspace=@faustjs/core --workspace=@faustjs/react --workspace=@faustjs/next",
"build": "npm run build --workspace=@faustjs/core --workspace=@faustjs/react --workspace=@faustjs/next --workspace=faust-nx",
"build:core": "npm run build --workspace=@faustjs/core",
"build:next": "npm run build --workspace=@faustjs/next",
"build:react": "npm run build --workspace=@faustjs/react",
"clean": "npm run clean --workspace=@faustjs/core --workspace=@faustjs/react --workspace=@faustjs/next",
"build:faustnx": "npm run build --workspace=faust-nx",
"clean": "npm run clean --workspace=@faustjs/core --workspace=@faustjs/react --workspace=@faustjs/next --workspace=faust-nx",
"clean:examples": "rimraf examples/**/node_modules",
"lint": "npm run lint --workspace=@faustjs/core --workspace=@faustjs/react --workspace=@faustjs/next",
"lint:fix": "npm run lint:fix --workspace=@faustjs/core --workspace=@faustjs/react --workspace=@faustjs/next",
Expand All @@ -33,7 +34,7 @@
"docs:clear": "npm run clear --prefix internal/website",
"docs:install": "cd internal/website && npm i && cd ..",
"docs:start": "npm start --prefix internal/website",
"test": "npm run build && npm test --workspace=@faustjs/core --workspace=@faustjs/react --workspace=@faustjs/next",
"test": "npm run build && npm test --workspace=@faustjs/core --workspace=@faustjs/react --workspace=@faustjs/next --workspace=faust-nx",
"test:coverage": "npm run build && npm run test:coverage --workspace=@faustjs/core --workspace=@faustjs/react --workspace=@faustjs/next",
"start": "npm run serve:prod --prefix internal/website",
"wpe-build": "cd internal/website && npm i && cd .. && npm run docs:build",
Expand Down
5 changes: 5 additions & 0 deletions packages/faust-nx/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
dist
jest.setup.js
jest.setup.d.ts
coverage
30 changes: 30 additions & 0 deletions packages/faust-nx/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module.exports = {
roots: ['<rootDir>/tests'],

// Adds Jest support for TypeScript using ts-jest.
transform: {
'^.+\\.tsx?$': 'ts-jest',
},

// Run code before each file in the suite is tested.
setupFilesAfterEnv: ['./jest.setup.ts'],

testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',

moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],

// ESM Support
// @link https://kulshekhar.github.io/ts-jest/docs/guides/esm-support/
extensionsToTreatAsEsm: ['.ts'],
globals: {
'ts-jest': {
useESM: true,
},
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
collectCoverage: true,
coverageReporters: ['json', 'html'],
passWithNoTests: true,
};
1 change: 1 addition & 0 deletions packages/faust-nx/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom'; // For custom matchers. See https://github.com/testing-library/jest-dom#custom-matchers.
37 changes: 28 additions & 9 deletions packages/faust-nx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,48 @@
"version": "0.0.1",
"private": true,
"description": "Experimental next version of Faust.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"main": "dist/cjs/index.js",
"module": "dist/mjs/index.js",
"types": "dist/cjs/index.d.ts",
"peerDependencies": {
"@apollo/client": ">=3.6.6",
"next": ">=12.1.6",
"react": ">=17.0.2",
"react-dom": ">=17.0.2",
"@apollo/client": ">=3.6.6"
"react-dom": ">=17.0.2"
},
"devDependencies": {
"@apollo/client": "^3.6.6",
"@testing-library/jest-dom": "^5.16.4",
"@types/jest": "^28.1.6",
"@types/node": "^18.0.6",
"@types/react": "^17.0.34",
"jest": "^28.1.3",
"next": "^12.1.6",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"@apollo/client": "^3.6.6"
"ts-jest": "^28.0.7"
},
"dependencies": {
"cookie": "^0.5.0",
"deepmerge": "^4.2.2",
"graphql": "^16.5.0",
"lodash": "^4.17.21"
"isomorphic-fetch": "^3.0.0",
"lodash": "^4.17.21",
"rimraf": "^3.0.2"
},
"scripts": {
"test": "npm test",
"build": "tsc -p tsconfig.json",
"dev": "tsc -p tsconfig.json --watch"
"build": "npm run clean && npm run ts && npm run ts:cjs && npm run package",
"clean": "rimraf dist",
"dev": "npm run ts:watch",
"package": "node ../../scripts/package.js",
"prepublish": "npm run build",
"test:coverage:ci": "npx jest --ci --json --coverage --testLocationInResults --outputFile=report.json",
"test:coverage": "jest --coverage",
"test:watch": "jest --watch",
"test": "jest",
"ts:cjs": "tsc -p tsconfig-cjs.json",
"ts:watch": "tsc -p . --watch",
"ts": "tsc -p ."
},
"repository": {
"type": "git",
Expand Down
63 changes: 63 additions & 0 deletions packages/faust-nx/src/auth/authorize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'isomorphic-fetch';
import isString from 'lodash/isString.js';
import { getWpUrl } from '../lib/getWpUrl.js';
import { getQueryParam, removeURLParam } from '../utils/index.js';
import { fetchAccessToken } from './client/accessToken.js';

export interface EnsureAuthorizationOptions {
redirectUri?: string;
loginPageUri?: string;
}

/* eslint-disable consistent-return */
/**
* Checks for an existing Access Token and returns one if it exists. Otherwise returns
* an object containing a redirect URI to send the client to for authorization.
*
* @export
* @param {string} EnsureAuthorizationOptions
* @returns {(string | { redirect: string })}
*/
export async function ensureAuthorization(
options?: EnsureAuthorizationOptions,
): Promise<
true | { redirect?: string | undefined; login?: string | undefined }
> {
const wpUrl = getWpUrl();
const { redirectUri, loginPageUri } = options || {};

// Get the authorization code from the URL if it exists
const code: string | undefined =
typeof window !== 'undefined'
? getQueryParam(window.location.href, 'code')
: undefined;

const unauthorized: { redirect?: string; login?: string } = {};

if (isString(redirectUri)) {
unauthorized.redirect = `${wpUrl}/generate?redirect_uri=${encodeURIComponent(
redirectUri,
)}`;
}

if (isString(loginPageUri)) {
unauthorized.login = loginPageUri;
}

const token = await fetchAccessToken(code);

if (!token) {
return unauthorized;
}

if (code) {
window.history.replaceState(
{},
document.title,
removeURLParam(window.location.href, 'code'),
);
}

return true;
}
/* eslint-enable consistent-return */
176 changes: 176 additions & 0 deletions packages/faust-nx/src/auth/client/accessToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import {
FAUSTNX_API_BASE_PATH,
TOKEN_ENDPOINT_PARTIAL_PATH,
} from '../../lib/constants.js';
import { isServerSide } from '../../utils/index.js';
import isString from 'lodash/isString.js';

export interface AccessToken {
/**
* Base 64 encoded access token
*/
token: string | undefined;
/**
* The time in seconds until the access token expires.
*/
expiration: number | undefined;
}

export type RefreshTimer = ReturnType<typeof setTimeout> | undefined;

/**
* The amount of time in seconds until the access token is fetched
* before it expires.
*
* For example, if the access token expires in 5 minutes (300 seconds), and
* this value is 60, then the access token will be refreshed at 240 seconds.
*
* This allows for enough time to fetch a new access token before it expires.
*
*/
export const TIME_UNTIL_REFRESH_BEFORE_TOKEN_EXPIRES = 60;

/**
* The setTimeout instance that refreshes the access token.
*/
let __REFRESH_TIMER: RefreshTimer = undefined;

export function getRefreshTimer(): RefreshTimer {
return __REFRESH_TIMER;
}

export function setRefreshTimer(timer: RefreshTimer): void {
__REFRESH_TIMER = timer;
}

/**
* The access token object
*/
let accessToken: AccessToken | undefined;

/**
* Get an access token from memory if one exists
*
* @returns {string | undefined}
*/
export function getAccessToken(): string | undefined {
return accessToken?.token;
}

/**
* Get an access token expiration from memory if one exists
*
* @returns {number | undefined}
*/
export function getAccessTokenExpiration(): number | undefined {
return accessToken?.expiration;
}

/**
* Set an access token and/or its expiration in memory
*
* @param {string} token
* @param {number} expiration
*
* @returns {void}
*/
export function setAccessToken(
token: string | undefined,
expiration: number | undefined,
): void {
if (isServerSide()) {
return;
}

accessToken = {
token,
expiration,
};
}

/**
* Creates the access token refresh timer that will fetch a new access token
* before the current one expires.
*
* @returns {void}
*/
export function setAccessTokenRefreshTimer(): void {
const currentTimeInSeconds = Math.floor(Date.now() / 1000);
const accessTokenExpirationInSeconds = getAccessTokenExpiration();

// If there is no access token/expiration, don't create a timer.
if (accessTokenExpirationInSeconds === undefined) {
return;
}

const secondsUntilExpiration =
accessTokenExpirationInSeconds - currentTimeInSeconds;
const secondsUntilRefresh =
secondsUntilExpiration - TIME_UNTIL_REFRESH_BEFORE_TOKEN_EXPIRES;

setRefreshTimer(
setTimeout(() => void fetchAccessToken(), secondsUntilRefresh * 1000),
);
}

/**
* Clears the current access token refresh timer if one exists.
*/
export function clearAccessTokenRefreshTimer(): void {
const timer = getRefreshTimer();
if (timer !== undefined) {
clearTimeout(timer);
}
}

/**
* Fetch an access token from the authorizeHandler middleware
*
* @export
* @param {string} code An authorization code to fetch an access token
*/
export async function fetchAccessToken(code?: string): Promise<string | null> {
let url = `${FAUSTNX_API_BASE_PATH}/${TOKEN_ENDPOINT_PARTIAL_PATH}`;

// Add the code to the url if it exists
if (isString(code) && code.length > 0) {
url += `?code=${code}`;
}

try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});

const result = (await response.json()) as {
accessToken: string;
accessTokenExpiration: number;
};

// If the response is not ok, clear the access token
if (!response.ok) {
setAccessToken(undefined, undefined);
return null;
}

setAccessToken(result.accessToken, result.accessTokenExpiration);

// If there is an existing refresh timer, clear it.
clearAccessTokenRefreshTimer();

/**
* Set a refresh timer to fetch a new access token before
* the current one expires.
*/
setAccessTokenRefreshTimer();

return result.accessToken;
} catch (error) {
setAccessToken(undefined, undefined);

return null;
}
}
2 changes: 2 additions & 0 deletions packages/faust-nx/src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './authorize.js';
export * from './client/accessToken.js';
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { WordPressTemplate } from '../getWordPressProps.js';

export interface FaustNXConfig {
templates: { [key: string]: WordPressTemplate };
disableLogging: boolean;
loginPagePath?: string;
}

let config = {};
Expand Down
File renamed without changes.
File renamed without changes.
6 changes: 6 additions & 0 deletions packages/faust-nx/index.ts → packages/faust-nx/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { FaustNXProvider } from './components/FaustNXProvider';
import { WordPressTemplate } from './components/WordPressTemplate';
import { getWordPressProps } from './getWordPressProps';
import { getConfig, setConfig, FaustNXConfig } from './config/index.js';
import { ensureAuthorization } from './auth/index.js';
import { authorizeHandler, logoutHandler, apiRouter } from './server/index.js';

export {
FaustNXProvider,
Expand All @@ -10,4 +12,8 @@ export {
getConfig,
setConfig,
FaustNXConfig,
ensureAuthorization,
authorizeHandler,
logoutHandler,
apiRouter,
};
3 changes: 3 additions & 0 deletions packages/faust-nx/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const FAUSTNX_API_BASE_PATH = '/api/faust';
export const TOKEN_ENDPOINT_PARTIAL_PATH = 'auth/token';
export const LOGOUT_ENDPOINT_PARTIAL_PATH = 'auth/logout';
3 changes: 3 additions & 0 deletions packages/faust-nx/src/lib/getWpSecret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function getWpSecret() {
return process.env.FAUSTNX_SECRET_KEY;
}
Loading

0 comments on commit 148a1dc

Please sign in to comment.