Skip to content

Commit

Permalink
Adding eslint-plugin-jest as a dev. dependency; Beginning to fix ESLint
Browse files Browse the repository at this point in the history
violations; Introducing support for i18n using i18n and react-i18next;
Extending the Authorization Actions for creating and updating
Authorization resources over REST; Introducing support for signing and
verifying JSON Web Tokens for generating auth. tokens on the
client-side; Providing adopters with the ability to specify a title for
the app.; Implementing a GooglePickerTree component as an alternative to
the ResourceTree for accessing Google Drive resources; Extending the
UploadForm in order to handle the Google Picker authorization process;
Adjusting styling (in response to integration testing the usage of a
modal in a Rails application)
  • Loading branch information
jrgriffiniii committed Feb 28, 2020
1 parent 30d6443 commit 7fcddcc
Show file tree
Hide file tree
Showing 17 changed files with 622 additions and 64 deletions.
6 changes: 5 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
PORT=3333
REACT_APP_API_URL=http://localhost:3000/browse
PORT=3001
REACT_APP_SECRET=$SECRET
REACT_APP_GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID
REACT_APP_GOOGLE_DEVELOPER_KEY=$GOOGLE_DEVELOPER_KEY
REACT_APP_GOOGLE_SCOPE=https://www.googleapis.com/auth/drive.readonly
25 changes: 25 additions & 0 deletions i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import i18n from "i18next"
import { initReactI18next } from "react-i18next"

const resources = {
en: {
translation: {
"Browse Everything": "Browse Everything"
}
}
}

i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
resources,
lng: "en",

keySeparator: false, // we do not use keys in form messages.welcome

interpolation: {
escapeValue: false // react already safes from xss
}
})

export default i18n
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
"version": "1.0.0",
"description": "Browse Everything user interface in React",
"license": "Apache-2.0",
"homepage" : "https://github.com/samvera-labs/browse-everything-redux-react",
"homepage": "https://github.com/samvera-labs/browse-everything-redux-react",
"private": true,
"dependencies": {
"@material-ui/core": "^4.4.2",
"@material-ui/icons": "^4.4.3",
"i18next": "^19.3.2",
"jsonwebtoken": "^8.5.1",
"load-script": "^1.0.0",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-i18next": "^11.3.3",
"react-scripts": "3.1.1",
"redux": "^4.0.4",
"redux-bees": "^0.1.11",
Expand Down
3 changes: 2 additions & 1 deletion src/__tests__/actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ describe('actions', () => {
]
const store = mockStore({ providers: [] })

store.dispatch(actions.updateProviders()).then(() => {
const promise = store.dispatch(actions.updateProviders())
const fulfilled = promise.then(() => {
expect(store.getActions()).toEqual(expectedActions)
})
})
Expand Down
7 changes: 6 additions & 1 deletion src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import {
clearProvider
} from './actions/providers'
import { createSession, clearSession } from './actions/sessions'
import { authorize, createAuthorization } from './actions/authorizations'
import {
authorize,
createAuthorization,
createClientAuthorization
} from './actions/authorizations'
import { getRootContainer, getContainer } from './actions/containers'
import {
createUpload,
Expand All @@ -23,6 +27,7 @@ export {
clearSession,
authorize,
createAuthorization,
createClientAuthorization,
getRootContainer,
getContainer,
createUpload,
Expand Down
77 changes: 76 additions & 1 deletion src/actions/authorizations.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as types from '../types'
import { config } from '../bees'
import { api, config } from '../bees'
import jwt from 'jsonwebtoken'

/**
* Authorizations
Expand Down Expand Up @@ -31,13 +32,87 @@ function receiveAuthorization(response) {
}
}

// This should probably be renamed
function updateAuthorization(updated) {
return dispatch => {
dispatch(requestAuthorization())

const attributes = {
id: updated.id,
code: updated.code
}
const patchData = {
data: {
type: 'post',
attributes
}
}

return api
.updateAuthorization({ id: updated.id }, patchData)
.then(response => {
const jsonData = response.body
const updatedWebToken = jwt.sign(jsonData, process.env.REACT_APP_SECRET, { algorithm: 'HS256' })
dispatch(authorize(updatedWebToken))
})
}
}

function receiveClientAuthorization(response, oauthToken) {
return (dispatch, getState) => {
// Here the auth. is not extracted from the HTTP response
const authToken = response.authToken
const webToken = jwt.verify(authToken, process.env.REACT_APP_SECRET, { algorithms: ['HS256'] })
// This does come with problems from the API
// Without updating the Authorization, the token cannot be passed to the API
// Updating the Authorization is a bit of a security risk, but I suppose that this is the case for any Authorization
// IDs (we just don't let clients retrieve an #index of all Authorizations)
const updated = { id: webToken.data.id, code: oauthToken }
dispatch(updateAuthorization(updated))

return {
type: types.RECEIVE_AUTHORIZATION,
isRequesting: false,
receivedAt: Date.now(),
authToken
}
}
}

export function createClientAuthorization(oauthToken) {
return (dispatch, getState) => {
dispatch(requestAuthorization())

const state = getState()
const provider = state.selectedProvider

// This is not a REST operation
const endpoint = config.baseUrl
const requestUrl = `${endpoint}/providers/${provider.id}/authorize`
const request = fetch(requestUrl)

return request.then(
response => {
const jsonResponse = response.json()
return jsonResponse.then(json => {
return dispatch(receiveClientAuthorization(json, oauthToken))
})
},
error => {
console.error(error)
}
)
}
}

export function createAuthorization() {
return (dispatch, getState) => {
dispatch(requestAuthorization())

const state = getState()
const provider = state.selectedProvider

// This is not a REST operation
const endpoint = config.baseUrl
const requestUrl = `${endpoint}/providers/${provider.id}/authorize`
const request = fetch(requestUrl)
Expand Down
5 changes: 3 additions & 2 deletions src/bees.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { buildApi, get, post } from 'redux-bees'
import { buildApi, get, patch, post } from 'redux-bees'

const apiEndpoints = {
getProviders: { method: get, path: '/providers' },
getProvider: { method: get, path: '/providers/:id' },
createSession: { method: post, path: '/sessions' },
getRootContainer: { method: get, path: '/sessions/:id/containers' },
getContainer: { method: get, path: '/sessions/:sessionId/containers/:id' },
createUpload: { method: post, path: '/uploads' }
createUpload: { method: post, path: '/uploads' },
updateAuthorization: { method: patch, path: '/authorizations/:id' }
}

export const config = {
Expand Down
18 changes: 13 additions & 5 deletions src/containers/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Container from '@material-ui/core/Container'
import { connect } from 'react-redux'
import Paper from '@material-ui/core/Paper'
import { withStyles } from '@material-ui/core/styles'
import { Trans } from 'react-i18next'

class App extends React.Component {
render() {
Expand All @@ -14,9 +15,11 @@ class App extends React.Component {
return (
<div className="App">
<Container maxWidth="lg">
<Typography variant="h3" component="h1" gutterBottom>
Browse Everything
</Typography>
{this.props.title && (
<Typography variant="h3" component="h1" gutterBottom>
<Trans>{this.props.title}</Trans>
</Typography>
)}
</Container>
<Container maxWidth="lg">
<Paper className={classes.root}>
Expand All @@ -41,6 +44,7 @@ class App extends React.Component {
App.propTypes = {
style: PropTypes.object,
classes: PropTypes.object,
title: PropTypes.string,
selectedProvider: PropTypes.object.isRequired, // This should be updated to currentProvider
providers: PropTypes.object.isRequired,
currentAuthToken: PropTypes.object.isRequired,
Expand All @@ -52,6 +56,10 @@ App.propTypes = {
onUpload: PropTypes.func
}

App.defaultProps = {
title: 'Browse Everything'
}

function mapStateToProps(state) {
const { selectedProvider } = state
const currentAuthToken = state.currentAuthToken || {
Expand Down Expand Up @@ -91,9 +99,9 @@ function mapStateToProps(state) {

const styles = {
root: {
padding: '30px'
padding: '30px',
paddingBottom: '2px'
}
}
const AppWithStyles = withStyles(styles)(App)

export default connect(mapStateToProps)(AppWithStyles)
15 changes: 8 additions & 7 deletions src/containers/AuthButton.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '@material-ui/core/Button'
import { makeStyles } from '@material-ui/core/styles'
import { withStyles } from '@material-ui/core/styles'

const useStyles = makeStyles({
const styles = {
root: {
alignSelf: 'center'
alignSelf: 'center',
float: 'right'
}
})
}

const AuthButton = ({ handleClick, authorizationUrl, disabled }) => {
const classes = useStyles
const AuthButton = ({ classes, handleClick, authorizationUrl, disabled }) => {
const textContent = disabled ? 'Authorized' : 'Sign In'

return (
Expand All @@ -29,9 +29,10 @@ const AuthButton = ({ handleClick, authorizationUrl, disabled }) => {
}

AuthButton.propTypes = {
classes: PropTypes.object.isRequired,
handleClick: PropTypes.func.isRequired,
authorizationUrl: PropTypes.string.isRequired,
disabled: PropTypes.bool
}

export default AuthButton
export default withStyles(styles)(AuthButton)
22 changes: 13 additions & 9 deletions src/containers/BrowseEverything.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Provider } from 'react-redux'
import configureStore from '../configureStore'
import App from './App'
import './BrowseEverything.css'

const store = configureStore()

/**
* Example of handler for updating the DOM once an upload has completed
*/
const handleUpload = function(event) {
console.log(event)
}

export default class BrowseEverything extends Component {
class BrowseEverything extends Component {
render() {
return (
<Provider store={store}>
<App onUpload={handleUpload} />
<App
onUpload={this.props.handleUpload}
title={this.props.title}
/>
</Provider>
)
}
}

BrowseEverything.propTypes = {
handleUpload: PropTypes.func,
title: PropTypes.string
}

export default BrowseEverything
Loading

0 comments on commit 7fcddcc

Please sign in to comment.