-
Notifications
You must be signed in to change notification settings - Fork 375
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #76 from sharetribe/update-v4.2.0-from-upstream
Update v4.2.0 from upstream
- Loading branch information
Showing
15 changed files
with
416 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
/** | ||
* This file contains server side endpoints that can be used to perform backend | ||
* tasks that can not be handled in the browser. | ||
* | ||
* The endpoints should not clash with the application routes. Therefore, the | ||
* endpoints are prefixed in the main server where this file is used. | ||
*/ | ||
|
||
const http = require('http'); | ||
const https = require('https'); | ||
const express = require('express'); | ||
const crypto = require('crypto'); | ||
const sharetribeSdk = require('sharetribe-flex-sdk'); | ||
const Decimal = require('decimal.js'); | ||
|
||
const CLIENT_ID = process.env.REACT_APP_SHARETRIBE_SDK_CLIENT_ID; | ||
const ROOT_URL = process.env.REACT_APP_CANONICAL_ROOT_URL; | ||
const CONSOLE_URL = | ||
process.env.SERVER_SHARETRIBE_CONSOLE_URL || 'https://flex-console.sharetribe.com'; | ||
const BASE_URL = process.env.REACT_APP_SHARETRIBE_SDK_BASE_URL; | ||
const TRANSIT_VERBOSE = process.env.REACT_APP_SHARETRIBE_SDK_TRANSIT_VERBOSE === 'true'; | ||
const USING_SSL = process.env.REACT_APP_SHARETRIBE_USING_SSL === 'true'; | ||
|
||
const router = express.Router(); | ||
|
||
// redirect_uri param used when initiating a login as authentication flow and | ||
// when requesting a token using an authorization code | ||
const loginAsRedirectUri = `${ROOT_URL.replace(/\/$/, '')}/api/login-as`; | ||
|
||
// Instantiate HTTP(S) Agents with keepAlive set to true. | ||
// This will reduce the request time for consecutive requests by | ||
// reusing the existing TCP connection, thus eliminating the time used | ||
// for setting up new TCP connections. | ||
const httpAgent = new http.Agent({ keepAlive: true }); | ||
const httpsAgent = new https.Agent({ keepAlive: true }); | ||
|
||
// Cookies used for authorization code authentication. | ||
const stateKey = `st-${CLIENT_ID}-oauth2State`; | ||
const codeVerifierKey = `st-${CLIENT_ID}-pkceCodeVerifier`; | ||
|
||
/** | ||
* Makes a base64 string URL friendly by | ||
* replacing unaccepted characters. | ||
*/ | ||
const urlifyBase64 = base64Str => | ||
base64Str | ||
.replace(/\+/g, '-') | ||
.replace(/\//g, '_') | ||
.replace(/=/g, ''); | ||
|
||
// Initiates an authorization code authentication flow. This authentication flow | ||
// enables marketplace operators that have an ongoing Console session to log | ||
// into their marketplace as a user of the marketplace. | ||
// | ||
// The authorization code is requested from Console and it is used to request a | ||
// token from the Flex Auth API. | ||
// | ||
// This endpoint will return a 302 to Console which requests the authorization | ||
// code. Console returns a 302 with the code to the `redirect_uri` that is | ||
// passed in this response. The request to the redirect URI is handled with the | ||
// `/login-as` endpoint. | ||
router.get('/initiate-login-as', (req, res) => { | ||
const userId = req.query.user_id; | ||
|
||
if (!userId) { | ||
return res.status(400).send('Missing query parameter: user_id.'); | ||
} | ||
if (!ROOT_URL) { | ||
return res.status(409).send('Marketplace canonical root URL is missing.'); | ||
} | ||
|
||
const state = urlifyBase64(crypto.randomBytes(32).toString('base64')); | ||
const codeVerifier = urlifyBase64(crypto.randomBytes(32).toString('base64')); | ||
const hash = crypto | ||
.createHash('sha256') | ||
.update(codeVerifier) | ||
.digest('base64'); | ||
const codeChallenge = urlifyBase64(hash); | ||
const authorizeServerUrl = `${CONSOLE_URL}/api/authorize-as`; | ||
|
||
const location = `${authorizeServerUrl}?\ | ||
response_type=code&\ | ||
client_id=${CLIENT_ID}&\ | ||
redirect_uri=${loginAsRedirectUri}&\ | ||
user_id=${userId}&\ | ||
state=${state}&\ | ||
code_challenge=${codeChallenge}&\ | ||
code_challenge_method=S256`; | ||
|
||
const cookieOpts = { | ||
maxAge: 1000 * 30, // 30 seconds | ||
secure: USING_SSL, | ||
}; | ||
|
||
res.cookie(stateKey, state, cookieOpts); | ||
res.cookie(codeVerifierKey, codeVerifier, cookieOpts); | ||
return res.redirect(location); | ||
}); | ||
|
||
// Works as the redirect_uri passed in an authorization code request. Receives | ||
// an authorization code and uses that to log in and redirect to the landing | ||
// page. | ||
router.get('/login-as', (req, res) => { | ||
const { code, state, error } = req.query; | ||
const storedState = req.cookies[stateKey]; | ||
|
||
if (state !== storedState) { | ||
return res.status(401).send('Invalid state parameter.'); | ||
} | ||
|
||
if (error) { | ||
return res.status(401).send(`Failed to authorize as a user, error: ${error}.`); | ||
} | ||
|
||
const codeVerifier = req.cookies[codeVerifierKey]; | ||
|
||
// clear state and code verifier cookies | ||
res.clearCookie(stateKey, { secure: USING_SSL }); | ||
res.clearCookie(codeVerifierKey, { secure: USING_SSL }); | ||
|
||
const baseUrl = BASE_URL ? { baseUrl: BASE_URL } : {}; | ||
const tokenStore = sharetribeSdk.tokenStore.expressCookieStore({ | ||
clientId: CLIENT_ID, | ||
req, | ||
res, | ||
secure: USING_SSL, | ||
}); | ||
|
||
const sdk = sharetribeSdk.createInstance({ | ||
transitVerbose: TRANSIT_VERBOSE, | ||
clientId: CLIENT_ID, | ||
httpAgent: httpAgent, | ||
httpsAgent: httpsAgent, | ||
tokenStore, | ||
typeHandlers: [ | ||
{ | ||
type: sharetribeSdk.types.BigDecimal, | ||
customType: Decimal, | ||
writer: v => new sharetribeSdk.types.BigDecimal(v.toString()), | ||
reader: v => new Decimal(v.value), | ||
}, | ||
], | ||
...baseUrl, | ||
}); | ||
|
||
sdk | ||
.login({ | ||
code, | ||
redirect_uri: loginAsRedirectUri, | ||
code_verifier: codeVerifier, | ||
}) | ||
.then(() => res.redirect('/')) | ||
.catch(() => res.status(401).send('Unable to authenticate as a user')); | ||
}); | ||
|
||
module.exports = router; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
src/components/LimitedAccessBanner/LimitedAccessBanner.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
@import '../../marketplace.css'; | ||
|
||
.root { | ||
background-color: #df492a; | ||
text-align: center; | ||
padding: 10px 20px 9px; | ||
} | ||
|
||
.text { | ||
margin: 0; | ||
display: inline-block; | ||
color: #fff; | ||
font-size: 16px; | ||
margin-bottom: 16px; | ||
line-height: 20px; | ||
} | ||
|
||
.button { | ||
background: #2a3d4b; | ||
margin: 0 16px; | ||
padding: 8px 16px; | ||
border-radius: 4px; | ||
font-size: 14px; | ||
color: #fff; | ||
border: 0; | ||
|
||
&:hover { | ||
text-decoration: none; | ||
background: #364f61; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import classNames from 'classnames'; | ||
import { FormattedMessage } from '../../util/reactIntl'; | ||
import { propTypes } from '../../util/types'; | ||
import { Button } from '../../components'; | ||
import { ensureCurrentUser } from '../../util/data'; | ||
|
||
import css from './LimitedAccessBanner.css'; | ||
|
||
// Due to the layout structure, do not render the banner on the following pages | ||
const disabledPages = ['SearchPage']; | ||
|
||
const LimitedAccessBanner = props => { | ||
const { | ||
rootClassName, | ||
className, | ||
isAuthenticated, | ||
authScopes, | ||
currentUser, | ||
onLogout, | ||
currentPage, | ||
} = props; | ||
const classes = classNames(rootClassName || css.root, className); | ||
const user = ensureCurrentUser(currentUser); | ||
|
||
const showBanner = | ||
user.id && | ||
isAuthenticated && | ||
authScopes && | ||
authScopes.length === 1 && | ||
authScopes[0] === 'user:limited' && | ||
!disabledPages.includes(currentPage); | ||
|
||
const { firstName, lastName } = user.attributes.profile; | ||
|
||
return showBanner ? ( | ||
<div className={classes}> | ||
<p className={css.text}> | ||
<FormattedMessage id="LimitedAccessBanner.message" values={{ firstName, lastName }} /> | ||
</p> | ||
<Button rootClassName={css.button} onClick={onLogout}> | ||
<FormattedMessage id="LimitedAccessBanner.logout" /> | ||
</Button> | ||
</div> | ||
) : null; | ||
}; | ||
|
||
LimitedAccessBanner.defaultProps = { | ||
rootClassName: null, | ||
className: null, | ||
currentUser: null, | ||
authScopes: [], | ||
currentPage: null, | ||
}; | ||
|
||
const { array, bool, func, string } = PropTypes; | ||
|
||
LimitedAccessBanner.propTypes = { | ||
rootClassName: string, | ||
className: string, | ||
isAuthenticated: bool.isRequired, | ||
authScopes: array, | ||
currentUser: propTypes.currentUser, | ||
onLogout: func.isRequired, | ||
currentPage: string, | ||
}; | ||
|
||
export default LimitedAccessBanner; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.