Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: #10935 seperate node scripts from fe #11037

Merged
merged 13 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ config-manager:
connect-session:
- packages/connect-session/**

connect-session-server:
- packages/connect-session-server/**

cra-template-foundations:
- packages/cra-template-foundations/**

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added .yarn/cache/fsevents-patch-19706e7e35-10.zip
Binary file not shown.
Binary file added .yarn/cache/fsevents-patch-6b67494872-10.zip
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3 changes: 3 additions & 0 deletions packages/connect-session-server/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { baseEslint } = require('@reapit/ts-scripts')

module.exports = baseEslint
1 change: 1 addition & 0 deletions packages/connect-session-server/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
4 changes: 4 additions & 0 deletions packages/connect-session-server/.snyk
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.14.1
ignore: {}
patch: {}
55 changes: 55 additions & 0 deletions packages/connect-session-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Connect Session Server

![lines](./src/tests/badges/badge-lines.svg) ![functions](./src/tests/badges/badge-functions.svg) ![branches](./src/tests/badges/badge-branches.svg) ![statements](./src/tests/badges/badge-statements.svg)

## Install

```bash
$ yarn add @reapit/connect-session-server
```

## Usage

For server side usage, we also export a Node module with a stripped down API that simply returns a promise from a connectAccessToken method. For a basic and slightly contrived example, see the simple Express app below:

```ts
import express, { Router, Request, Response } from 'express'
import bodyParser from 'body-parser'
import cors from 'cors'
import { ReapitConnectServerSession, ReapitConnectServerSessionInitializers } from '@reapit/connect-session-server'
import config from './config.json'

const router = Router()

const { connectClientId, connectClientSecret, connectOAuthUrl } = config as ReapitConnectServerSessionInitializers

// Instance as a singleton as token will be cached within the class (prevents duplicate requests for access token)
const reapitConnectSession = new ReapitConnectServerSession({
connectClientId,
connectClientSecret,
connectOAuthUrl,
})

router.get('/get-access-token', async (req: Request, res: Response) => {
const accessToken = await reapitConnectSession.connectAccessToken()
// Do some stuff with my access token here, will just return it to the user as an example
res.status(200)
res.send(accessToken)
res.end()
})

const app = express()

app.use(cors())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
app.use('/', router)

app.listen('3000', () => {
console.log('App is listening on 3000')
})
```

As per the browser usage, you will need to instantiate the class with your initializers, in this case `connectClientId`, `connectOAuthUrl` (in the same way as the browser module), but with the addition of the `connectClientSecret` you obtain from your app listing page.

The module will fetch and refresh your session as the token expires, caching it locally to minimise calls to Reapit Connect token endpoint.
27 changes: 27 additions & 0 deletions packages/connect-session-server/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { pathsToModuleNameMapper } = require('ts-jest')
const { compilerOptions } = require('./tsconfig.json')
const { jestGlobalConfig } = require('@reapit/ts-scripts')

module.exports = {
...jestGlobalConfig,
modulePathIgnorePatterns: ['<rootDir>[/\\\\](node_modules|public|dist)[/\\\\]'],
moduleNameMapper: {
...pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>/',
}),
},
coveragePathIgnorePatterns: [
'<rootDir>[/\\\\](node_modules|src/tests|src/__mocks__)[/\\\\]',
'<rootDir>/src/types.ts',
'<rootDir>/src/index.ts',
'.d.ts',
],
coverageThreshold: {
global: {
branches: 69,
functions: 96,
lines: 91,
statements: 91,
},
},
}
51 changes: 51 additions & 0 deletions packages/connect-session-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@reapit/connect-session-server",
"packageManager": "[email protected]",
"description": "Server OAuth Flow for Reapit Connect",
"keywords": [
"reapit-connect",
"server",
"connect-session"
],
"version": "1.0.0",
"main": "dist/index.js",
"homepage": "https://github.com/reapit/foundations#readme",
"bugs": {
"url": "https://github.com/reapit/foundations/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/reapit/foundations.git"
},
"devDependencies": {
"@types/eslint": "^8",
"@types/jest": "^29.5.12",
"@types/node": "^20.12.7",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0",
"jest-coverage-badges": "^1.1.2",
"prettier": "^3.2.5",
"ts-jest": "^29.1.2",
"tsup": "^8.0.2",
"typescript": "^5.4.5"
},
"scripts": {
"build": "tsup",
"lint": "eslint --cache --ext=ts,js src",
"test": "cross-env jest --watch --colors",
"check": "tsc --noEmit --skipLibCheck",
"release": "echo '...skipping...'",
"deploy": "echo '...skipping...'",
"publish": "yarn npm publish --access public",
"conf": "echo '...skipping...'",
"commit": "yarn test --coverage --no-cache --silent --forceExit --detectOpenHandles --runInBand --watch=false && jest-coverage-badges --input src/tests/coverage/coverage-summary.json --output src/tests/badges && yarn lint --fix && yarn check"
},
"dependencies": {
"axios": "^1.6.8",
"jwt-decode": "^4.0.0"
}
}
47 changes: 47 additions & 0 deletions packages/connect-session-server/src/__mocks__/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ReapitConnectServerSessionInitializers, CoginitoSession } from '../'
import base64 from 'base-64'

export const createMockToken = (token: { [s: string]: any } | string): string =>
`${base64.encode('{}')}.${base64.encode(typeof token === 'string' ? token : JSON.stringify(token))}.${base64.encode(
'{}',
)}`

export const mockLoginIdentity = {
email: '[email protected]',
name: 'name',
developerId: 'SOME_DEVELOPER_ID',
clientId: 'SOME_CLIENT_ID',
adminId: 'SOME_ADMIN_ID',
userCode: 'SOME_USER_ID',
orgName: 'SOME_ORG_NAME',
orgId: 'SOME_ORG_ID',
groups: ['AgencyCloudDeveloperEdition', 'OrganisationAdmin', 'ReapitUser', 'ReapitDeveloper', 'ReapitDeveloperAdmin'],
offGroupIds: 'MKV',
offGrouping: true,
offGroupName: 'Cool Office Group',
officeId: 'MVK',
orgProduct: 'agencyCloud',
agencyCloudId: 'SOME_AC_ID',
}

export const mockServerInitializers: ReapitConnectServerSessionInitializers = {
connectClientId: 'SOME_CLIENT_ID',
connectOAuthUrl: 'SOME_URL',
connectClientSecret: 'SOME_SECRET',
}

export const mockTokenResponse: CoginitoSession = {
access_token: createMockToken({
exp: Math.round(new Date().getTime() / 1000) + 360, // time now + 6mins - we refresh session if expiry within 5mins
}),
refresh_token: 'SOME_REFRESH_TOKEN',
id_token: createMockToken({
name: mockLoginIdentity.name,
email: mockLoginIdentity.email,
'custom:reapit:developerId': mockLoginIdentity.developerId,
'custom:reapit:clientCode': mockLoginIdentity.clientId,
'custom:reapit:marketAdmin': mockLoginIdentity.adminId,
'custom:reapit:userCode': mockLoginIdentity.userCode,
'cognito:groups': mockLoginIdentity.groups,
}),
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReapitConnectServerSession } from '../index'
import { mockTokenResponse, mockServerInitializers, createMockToken } from '../../__mocks__/session'
import { ReapitConnectServerSession } from './'
import { mockTokenResponse, mockServerInitializers, createMockToken } from './__mocks__/session'
import axios from 'axios'

jest.mock('idtoken-verifier', () => ({
Expand All @@ -8,8 +8,6 @@ jest.mock('idtoken-verifier', () => ({
},
}))

jest.mock('../../utils/verify-decode-id-token')

jest.mock('axios', () => ({
post: jest.fn(),
}))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
import axios from 'axios'
import { CoginitoAccess, ReapitConnectServerSessionInitializers } from '../types'
import decode from 'jwt-decode'
import { DecodedToken } from '../utils'
import { jwtDecode } from 'jwt-decode'

export class ReapitConnectServerSession {
// Static constants
static TOKEN_EXPIRY = Math.round(new Date().getTime() / 1000) + 3300 // 55 minutes from now - expiry is 1hr, 5mins
// to allow for clock drift - unused, not removing as don't want to make any breaking changes
export interface CoginitoSession {
access_token: string
id_token: string
refresh_token: string
error?: string
}

export interface ReapitConnectServerSessionInitializers {
connectOAuthUrl: string
connectClientId: string
connectClientSecret: string
}

// Private cached variables, I don't want users to reference these directly or it will get confusing.
// and cause bugs
private connectOAuthUrl: string
private connectClientId: string
private connectClientSecret: string
export interface CoginitoAccess {
sub: string
'cognito:groups': string[]
token_use: string
scope: string
auth_time: number
iss: string
exp: number
iat: number
version: number
jti: string
client_id: string
username: string
}

export type DecodedToken<T> = {
aud: string
} & T

export class ReapitConnectServerSession {
private readonly connectOAuthUrl: string
private readonly connectClientId: string
private readonly connectClientSecret: string
private accessToken: string | null

constructor({ connectClientId, connectClientSecret, connectOAuthUrl }: ReapitConnectServerSessionInitializers) {
Expand All @@ -27,7 +51,7 @@ export class ReapitConnectServerSession {
// Check on access token to see if has expired - they last 1hr only before I need to refresh
private get accessTokenExpired() {
if (this.accessToken) {
const decoded = decode<DecodedToken<CoginitoAccess>>(this.accessToken)
const decoded = jwtDecode<DecodedToken<CoginitoAccess>>(this.accessToken)
const expiry = decoded['exp']
// 5mins to allow for clock drift
const fiveMinsFromNow = Math.round(new Date().getTime() / 1000) + 300
Expand All @@ -42,8 +66,11 @@ export class ReapitConnectServerSession {
try {
const base64Encoded = Buffer.from(`${this.connectClientId}:${this.connectClientSecret}`).toString('base64')
const session = await axios.post(
`${this.connectOAuthUrl}/token?grant_type=client_credentials&client_id=${this.connectClientId}`,
{},
`${this.connectOAuthUrl}/token`,
new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.connectClientId,
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Expand All @@ -56,7 +83,7 @@ export class ReapitConnectServerSession {
throw new Error(session.data.error)
}

if (session.data && session.data.access_token) {
if (session?.data?.access_token) {
return session.data.access_token
}
throw new Error('No access token returned by Reapit Connect')
Expand All @@ -67,7 +94,6 @@ export class ReapitConnectServerSession {

// The main method for fetching an accessToken in an app.
public async connectAccessToken(): Promise<string | void> {
// Ideally, if I have a valid accessToken, just return it
if (!this.accessTokenExpired) {
return this.accessToken as string
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions packages/connect-session-server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"target": "ES2018",
"typeRoots": ["./src/core/definitions.d.ts", "node_modules/@types", "../../node_modules/@types"],
"baseUrl": "./",
"sourceRoot": "/",
"paths": {
"@reapit/utils-nest": ["../utils-nest/src"],
"@reapit/*": ["../*"]
},
"declaration": true,
"declarationDir": "dist",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false,
"outDir": "./dist"
},
"include": ["src"],
"exclude": ["public", "dist", "src/scripts", "node_modules", "src/tests/coverage", "../connect-session/**/*"]
}
16 changes: 16 additions & 0 deletions packages/connect-session-server/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineConfig } from 'tsup';
import fs from 'fs';

const pkgJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'))

export default defineConfig({
entry: ['src/index.ts'],
target: 'node16',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node 18 right? Think 16 is out of support

outDir: 'dist',
clean: true,
minify: process.env.NODE_ENV === 'production',
esbuildOptions: (opts) => {
opts.resolveExtensions = ['.ts', '.mjs', '.js'];
},
noExternal: Object.keys(pkgJson.dependencies),
})
Loading
Loading