Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,36 @@ Once logged in, visit [https://127.0.0.1:8080/editor](https://127.0.0.1:8080/edi

`yarn test` or `yarn test:ci`

### Deployment and Setup

For deployment, there are several places you can set configuration options. Obojobo uses a hybrid of json files and environment variables. We are moving more toward using more environment variables. However, json config files can be found in several obojobo packages (they are usually in server/config/*.json files). Below are a set of environment variables that we recommend using

- `CDN_ASSET_HOST`: If you want to host compiled browser assets (js/css/etc) on a CDN. Ex value: `https://site.com`
- `NODE_ENV`: determines which environment from the config files will be used. Use production unless you have a use for multiple environments.
- `DB_USER`: db user
- `DB_PASS`: db pass
- `DB_HOST`: db host
- `DB_NAME`: db name
- `DB_PORT`: db port
- `DB_SSL_JSON`: enable or disable using ssl to connect to the database
- `DB_TIMEOUT`: ex: 29000. Used to set the pg-promise settings for query_timeout and statement_timeout.
- `OBO_LTI_KEYS_JSON`: ex: '{"key1":"secret1","key2":"secret2}'. Use json string to define valid lti key/secret pairs.
- `OBO_LTI_USERNAME_PARAM`: What lti launch param do you want to use as the username in obojobo. ex: `user_id` or `lis_person_sourcedid`
- `OBO_COOKIE_SECRET`: Randomized secret key that is used to encrypt cookies. See the docs for express-session.
- `OBO_COOKIE_SECURE`: Set to true when using https. See docs for express-session. Used to set secure, sameSite and httpOnly options.
- `DEBUG`: Logging Verbosity. Use obojobo_server:* for more output. ex: `obojobo_server:error,obojobo_server:warn`
- `YARN_PRODUCTION`: Set to true. Makes sure yarn install includes dev dependencies.
- `OBO_DEMO_PURGE_MODE`: Automatically purge old data for demo purposes or to limit database growth. Additional addon setup required. ex: disabled (default), DANGER-delete-HISTORY-data, DANGER-delete-ALL-data
- `OBO_DEMO_PURGE_DAYS_AGO`: If purge mode is enabled, purge data older than this many days. ex: 7
- `OBO_EDITLOCK_TIME_UNTIL_RELEASE_MINUTES`: Editor lock: period of inactivity to release the lock. ex: 45
- `OBO_EDITLOCK_TIME_UNTIL_WARN_MINUTES`: Editor lock: period of inactivity to warn the user about lock release. ex: 40
- `OBO_EDITLOCK_DB_LOCK_DURATION`: Editor lock: period of time a lock lasts unless it is renewed. ex: 5
- `OBO_LTI_GUID`: A GUID unique to this install of Obojobo. Google LTI launch param tool_consumer_instance_guid for more details. ex: `edu.your-school.obojobo`
- `MATERIA_OAUTH_KEY`: LTI Oauth key used just for the optional Materia integration.
- `MATERIA_OAUTH_SECRET`: LTI Oauth secret used the optional Materia integration.
- `MATERIA_HOST`: URL for the optional Materia integration. ex: https://your.materia.com
- `OBO_OPTIONAL_NODES`: Comma separated list of optional nodes that are already installed to enable. ex: `*` or `obojobo-chunks-materia`

### Special Thanks

Support for this work was provided by the National Science Foundation Scholarships in Science, Technology, Engineering, and Mathematics (S-STEM) program under [Award No.1643835](https://www.nsf.gov/awardsearch/showAward?AWD_ID=1643835). Any opinions, findings, conclusions and recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.
Expand Down
13 changes: 13 additions & 0 deletions packages/app/obojobo-express/__tests__/asset_resolver.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,20 @@ const { assetForEnv } = require('../server/asset_resolver')
describe('Asset Resolver', () => {
const originalNODE_ENV = process.env.NODE_ENV
const originalIS_WEBPACK = process.env.IS_WEBPACK
const originalCDN_ASSET_HOST = process.env.CDN_ASSET_HOST

afterAll(() => {
process.env.NODE_ENV = originalNODE_ENV
process.env.IS_WEBPACK = originalIS_WEBPACK
process.env.CDN_ASSET_HOST = originalCDN_ASSET_HOST
})

beforeEach(() => {
jest.resetModules()
delete process.env.NODE_ENV
delete process.env.ASSET_ENV
delete process.env.IS_WEBPACK
delete process.env.CDN_ASSET_HOST
})

test('assetForEnv builds pattern for dev server', () => {
Expand Down Expand Up @@ -90,4 +93,14 @@ describe('Asset Resolver', () => {
expect(webpackAssetPath('subdir/test.json')).toBe('/static-from-manifest/subdir/test.json')
expect(webpackAssetPath('asset-that-doesnt-exist.js')).toBe(undefined) //eslint-disable-line no-undefined
})

test('CDN_ASSET_HOST is prepended to manifest assets when present', () => {
process.env.IS_WEBPACK = 'false'
process.env.CDN_ASSET_HOST = 'https://test-cdn-value.com'
const { webpackAssetPath } = require('../server/asset_resolver')

expect(webpackAssetPath('test.js')).toBe(
'https://test-cdn-value.com/static-from-manifest/test.js'
)
})
})
13 changes: 10 additions & 3 deletions packages/app/obojobo-express/server/asset_resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,19 @@ const getEnv = forceEnvTo => {
return (env || NODE_ENV).toLowerCase()
}

const resolveFromManifest = assetName => {
// eslint-disable-next-line no-undefined
if (!manifest[assetName]) return undefined
return `${process.env.CDN_ASSET_HOST || ''}${manifest[assetName]}`
}

// use the original name in the static path
const resolveFromWebpackDevServer = assetName => `/static/${assetName}`

const IS_WEBPACK = process.env.IS_WEBPACK === 'true'
// NOTE: manifest created via `yarn build`
const manifest = IS_WEBPACK ? {} : require('./public/compiled/manifest.json')
const webpackAssetPath = IS_WEBPACK
? assetName => `/static/${assetName}` // use the original name in the static path
: assetName => manifest[assetName] // return path from the manifest
const webpackAssetPath = IS_WEBPACK ? resolveFromWebpackDevServer : resolveFromManifest

module.exports = {
assetForEnv,
Expand Down
15 changes: 12 additions & 3 deletions packages/app/obojobo-repository/server/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ oboEvents.on('HTTP_NOT_AUTHORIZED', ({ req, res, next }) => {
title: 'Not Authorized',
children: 'You do not have the permissions required to view this page.',
currentUser: req.currentUser,
appCSSUrl
appCSSUrl,
globals: {
staticAssetUrl: process.env.CDN_ASSET_HOST || ''
}
}
res.render('pages/page-error.jsx', props)
})
Expand All @@ -42,7 +45,10 @@ oboEvents.on('HTTP_NOT_FOUND', ({ req, res, next }) => {
title: 'Not Found',
children: "The page you requested doesn't exist.",
currentUser: req.currentUser,
appCSSUrl
appCSSUrl,
globals: {
staticAssetUrl: process.env.CDN_ASSET_HOST || ''
}
}
res.render('pages/page-error.jsx', props)
})
Expand All @@ -56,7 +62,10 @@ oboEvents.on('HTTP_UNEXPECTED', ({ req, res, next }) => {
title: 'Unexpected Server Error',
children: 'There was an internal server error.',
currentUser: req.currentUser,
appCSSUrl
appCSSUrl,
globals: {
staticAssetUrl: process.env.CDN_ASSET_HOST || ''
}
}
res.render('pages/page-error.jsx', props)
})
Expand Down
5 changes: 4 additions & 1 deletion packages/app/obojobo-repository/server/routes/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ router
currentUser: req.currentUser,
// must use webpackAssetPath for all webpack assets to work in dev and production!
appCSSUrl: webpackAssetPath('dashboard.css'),
appJsUrl: webpackAssetPath('dashboard.js')
appJsUrl: webpackAssetPath('dashboard.js'),
globals: {
staticAssetUrl: process.env.CDN_ASSET_HOST || ''
}
}
res.render('pages/page-dashboard-server.jsx', props)
})
Expand Down
21 changes: 18 additions & 3 deletions packages/app/obojobo-repository/server/routes/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
requireDraftId,
getCurrentUser
} = require('obojobo-express/server/express_validators')
const { populateClientGlobals } = require('../../shared/react-utils')

const publicLibCollectionId = require('../../shared/publicLibCollectionId')

Expand All @@ -21,7 +22,10 @@ router
const props = {
currentUser: req.currentUser,
// must use webpackAssetPath for all webpack assets to work in dev and production!
appCSSUrl: webpackAssetPath('homepage.css')
appCSSUrl: webpackAssetPath('homepage.css'),
globals: {
staticAssetUrl: process.env.CDN_ASSET_HOST || ''
}
}
res.render('pages/page-homepage.jsx', props)
})
Expand Down Expand Up @@ -102,7 +106,10 @@ router
const props = {
currentUser: req.currentUser,
// must use webpackAssetPath for all webpack assets to work in dev and production!
appCSSUrl: webpackAssetPath('repository.css')
appCSSUrl: webpackAssetPath('repository.css'),
globals: {
staticAssetUrl: process.env.CDN_ASSET_HOST || ''
}
}
res.render('pages/page-login.jsx', props)
})
Expand All @@ -122,8 +129,12 @@ router
pageCount: 1,
currentUser: req.currentUser,
// must use webpackAssetPath for all webpack assets to work in dev and production!
appCSSUrl: webpackAssetPath('repository.css')
appCSSUrl: webpackAssetPath('repository.css'),
globals: {
staticAssetUrl: process.env.CDN_ASSET_HOST || ''
}
}
populateClientGlobals(props)
res.render('pages/page-library.jsx', props)
})
.catch(res.unexpected)
Expand Down Expand Up @@ -159,8 +170,12 @@ router
// must use webpackAssetPath for all webpack assets to work in dev and production!
appCSSUrl: webpackAssetPath('repository.css'),
appJsUrl: webpackAssetPath('page-module.js'),
globals: {
staticAssetUrl: process.env.CDN_ASSET_HOST || ''
},
canCopy
}

res.render('pages/page-module-server.jsx', props)
} catch (e) {
res.unexpected(e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ jest.mock('../models/collection')
jest.mock('../models/draft_summary')
jest.mock('obojobo-express/server/models/user')
jest.mock('../models/draft_permissions')
jest.mock('../../shared/react-utils')
jest.mock('trianglify')
jest.unmock('fs') // need fs working for view rendering
jest.unmock('express') // we'll use supertest + express for this
Expand All @@ -17,14 +18,11 @@ jest.mock(
jest.setTimeout(10000) // extend test timeout?

const publicLibCollectionId = require('../../shared/publicLibCollectionId')

let trianglify

let Collection
let DraftSummary
let UserModel
let DraftPermissions

// override requireCurrentUser for tests to provide our own user
let mockCurrentUser

Expand Down
5 changes: 4 additions & 1 deletion packages/app/obojobo-repository/server/routes/stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ router
currentUser: req.currentUser,
// must use webpackAssetPath for all webpack assets to work in dev and production!
appCSSUrl: webpackAssetPath('stats.css'),
appJsUrl: webpackAssetPath('stats.js')
appJsUrl: webpackAssetPath('stats.js'),
globals: {
staticAssetUrl: process.env.CDN_ASSET_HOST || ''
}
}
res.render('pages/page-stats-server.jsx', props)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
require('./module-image.scss')

const React = require('react')
const clientGlobals = require('../util/client-globals')

const ModuleImage = props => (
<div className="repository--module-icon--image">
<img src={`/library/module-icon/${props.id}`} width="100%" height="100%" />
<img
src={`${clientGlobals.staticAssetUrl || ''}/library/module-icon/${props.id}`}
width="100%"
height="100%"
/>
</div>
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { hydrateElWithoutStore } from '../../react-utils'
import PageModule from './page-module.jsx'
import { hydrateEl } from '../../react-utils'
import PageModule from './page-module-hoc'
import AboutModuleReducer from '../../reducers/about-module-reducer'

hydrateElWithoutStore(PageModule, '#react-hydrate-root')
hydrateEl(PageModule, AboutModuleReducer, '#react-hydrate-root')
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
jest.mock('../../react-utils')
jest.mock('./page-module')
jest.mock('../../reducers/dashboard-reducer')
jest.mock('./page-module-hoc')
jest.mock('../../reducers/about-module-reducer')

const ReactUtils = require('../../react-utils')
const PageModule = require('./page-module')
const PageModule = require('./page-module-hoc')
const AboutModuleReducer = require('../../reducers/about-module-reducer')

describe('Client-side Module Page', () => {
test('passes the right arguments to ReactUtils.hydrateElWithoutStore', () => {
// just need to require this, it will run itself
require('./page-module-client')

expect(ReactUtils.hydrateElWithoutStore).toHaveBeenCalledWith(PageModule, '#react-hydrate-root')
expect(ReactUtils.hydrateEl).toHaveBeenCalledWith(
PageModule,
AboutModuleReducer,
'#react-hydrate-root'
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const PageModule = require('./page-module')
const connect = require('react-redux').connect
/* istanbul ignore next */
const mapStoreStateToProps = state => state
module.exports = connect(
mapStoreStateToProps,
{}
)(PageModule)
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const React = require('react')
const DefaultLayout = require('../layouts/default')
const { convertPropsToString } = require('../../react-utils')
const { propsToStore, createCommonReactApp, convertPropsToString } = require('../../react-utils')
const PageModule = require('./page-module-hoc')
const AboutModuleReducer = require('../../reducers/about-module-reducer')

const PageModuleServer = props => {
return (
Expand All @@ -10,7 +12,9 @@ const PageModuleServer = props => {
appScriptUrl={props.appJsUrl}
appCSSUrl={props.appCSSUrl}
>
<span id="react-hydrate-root" data-react-props={convertPropsToString(props)} />
<span id="react-hydrate-root" data-react-props={convertPropsToString(props)}>
{createCommonReactApp(PageModule, propsToStore(AboutModuleReducer, props))}
</span>
</DefaultLayout>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ describe('Server-side Module Page', () => {
const mockProps = {
appJsUrl: '/path/to/js',
appCSSUrl: '/path/to/css',
globals: {
staticAssetUrl: 'https://cdn.sample.com'
},
module: {
title: 'mockModuleTitle'
}
Expand Down
19 changes: 18 additions & 1 deletion packages/app/obojobo-repository/shared/react-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ const { Provider } = require('react-redux')
const { createStore, applyMiddleware, compose } = require('redux')
const { middleware } = require('redux-pack')

// if initial state contains a globals object, we need to copy
// those globals into clientGlobals so that they can be loaded
// as global constants anywhere in the application.
function populateClientGlobals(initialState) {
if (initialState && initialState.globals) {
const clientGlobals = require('../shared/util/client-globals')
for (const property in initialState.globals) {
clientGlobals[property] = initialState.globals[property]
}
// freeze to prevent changes
Object.freeze(clientGlobals)
}
}

// used in the browser to hydrate a SSR page w/o Redux
function hydrateElWithoutStore(Component, domSelector) {
const domEl = document.querySelector(domSelector)
Expand Down Expand Up @@ -32,7 +46,9 @@ function propsToStore(reducer, initialState, optionalMiddleware) {
return createStore(reducer, initialState, preppedMiddleware)
}

// this function is called on both the server and the client
function createCommonReactApp(Component, store) {
populateClientGlobals(store.getState())
const app = React.createElement(Component)
const provider = React.createElement(Provider, { store }, app)
return provider
Expand All @@ -53,5 +69,6 @@ module.exports = {
hydrateElWithoutStore,
propsToStore,
createCommonReactApp,
convertPropsToString
convertPropsToString,
populateClientGlobals
}
Loading