From ecd2f19157bb9c7d95acf8c78930b4345cc00af9 Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Thu, 2 Jan 2020 11:43:46 +0200 Subject: [PATCH 01/16] Add handlebars v4.5.3 to resolutions --- package.json | 3 ++- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index e213d3845..9381deb41 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,8 @@ "resolutions": { "react-dates/lodash": "^4.17.14", "react-google-maps/lodash": "^4.17.14", - "react-test-renderer": "^16.9.0" + "react-test-renderer": "^16.9.0", + "handlebars": "^4.5.3" }, "nodemonConfig": { "execMap": { diff --git a/yarn.lock b/yarn.lock index ac7520d80..b3f7f9787 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5025,10 +5025,10 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" - integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== +handlebars@^4.1.2, handlebars@^4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482" + integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA== dependencies: neo-async "^2.6.0" optimist "^0.6.1" From 04832a55ccd517e5e5a2496150ac1d685eb8d5a2 Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Thu, 2 Jan 2020 13:11:07 +0200 Subject: [PATCH 02/16] Add serialize-javascript v2.1.1 to resolutions --- package.json | 3 ++- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9381deb41..47e08897b 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,8 @@ "react-dates/lodash": "^4.17.14", "react-google-maps/lodash": "^4.17.14", "react-test-renderer": "^16.9.0", - "handlebars": "^4.5.3" + "handlebars": "^4.5.3", + "serialize-javascript": "^2.1.1" }, "nodemonConfig": { "execMap": { diff --git a/yarn.lock b/yarn.lock index b3f7f9787..94b6214f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10034,10 +10034,10 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" -serialize-javascript@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.8.0.tgz#9515fc687232e2321aea1ca7a529476eb34bb480" - integrity sha512-3tHgtF4OzDmeKYj6V9nSyceRS0UJ3C7VqyD2Yj28vC/z2j6jG5FmFGahOKMD9CrglxTm3tETr87jEypaYV8DUg== +serialize-javascript@^1.7.0, serialize-javascript@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" + integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== serve-index@^1.7.2: version "1.9.1" From 7a75102184f03247a16d87313a7d3883d0c9ff6f Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Fri, 3 Jan 2020 12:02:07 +0200 Subject: [PATCH 03/16] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d9f48143..0c589cbec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ way to update this template, but currently, we follow a pattern: ## Upcoming version 2020-XX-XX +- [change] Add `handlebars` 4.5.3 and `serialize-javascript` 2.1.1 to resolutions in `package.json`. + [#1251](https://github.com/sharetribe/ftw-daily/pull/1251) + ## [v4.1.0] 2020-02-03 - [fix] Remove unused 'invalid' prop that breaks some versions of Final Form @@ -22,6 +25,8 @@ way to update this template, but currently, we follow a pattern: - [add] Add missing countries (e.g. MX and JP) to `StripeBankAccountTokenInput` validations. [#1250](https://github.com/sharetribe/ftw-daily/pull/1250) + [v4.0.1]: https://github.com/sharetribe/flex-template-web/compare/v4.0.0...v4.0.1 + ## [v4.0.0] 2019-12-19 - [change] Use Stripe's [Connect onboarding](https://stripe.com/docs/connect/connect-onboarding) for From 1d8b7a65596318f89df92bc927b1653829edee2c Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Fri, 31 Jan 2020 15:58:56 +0200 Subject: [PATCH 04/16] Add login as user authentication endpoints Add two endpoints to the server to enable logging in as a user using the OAuth2 authorization_code grant type. --- server/apiRouter.js | 156 ++++++++++++++++++++++++++++++++++++++++++++ server/index.js | 4 ++ 2 files changed, 160 insertions(+) create mode 100644 server/apiRouter.js diff --git a/server/apiRouter.js b/server/apiRouter.js new file mode 100644 index 000000000..9e668c6fb --- /dev/null +++ b/server/apiRouter.js @@ -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 + * enpoints 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 authenitcation flow and +// when requesting a token useing 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 marketpalce. +// +// The authroization 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 reponse. 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; diff --git a/server/index.js b/server/index.js index f45d5f7b5..ef4fc89f7 100644 --- a/server/index.js +++ b/server/index.js @@ -32,6 +32,7 @@ const sharetribeSdk = require('sharetribe-flex-sdk'); const Decimal = require('decimal.js'); const sitemap = require('express-sitemap'); const auth = require('./auth'); +const apiRouter = require('./apiRouter'); const renderer = require('./renderer'); const dataLoader = require('./dataLoader'); const fs = require('fs'); @@ -132,6 +133,9 @@ if (!dev) { } } +// Server-side routes that do not render the application +app.use('/api', apiRouter); + const noCacheHeaders = { 'Cache-control': 'no-cache, no-store, must-revalidate', }; From 0dea624171c21e030aa87a6443e3fd29b82cf08c Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Tue, 11 Feb 2020 11:16:42 +0200 Subject: [PATCH 05/16] Update Flex SDK --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 47e08897b..914e5aaf7 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "redux": "^4.0.1", "redux-thunk": "^2.3.0", "seedrandom": "^3.0.3", - "sharetribe-flex-sdk": "^1.8.0", + "sharetribe-flex-sdk": "^1.9.0", "sharetribe-scripts": "3.1.1", "smoothscroll-polyfill": "^0.4.0", "source-map-support": "^0.5.9", diff --git a/yarn.lock b/yarn.lock index 94b6214f8..a1346ff3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10132,10 +10132,10 @@ shallowequal@1.1.0: resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== -sharetribe-flex-sdk@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/sharetribe-flex-sdk/-/sharetribe-flex-sdk-1.8.0.tgz#d7f24bf416f15c5f20d7b6830e0d6e5832082b43" - integrity sha512-600NLlvQamxQP8hKhyWlytl5cycoYxAMce1xXAFxTMAoNiOyLEh9K/6I4nrf9AowpDL+2IoPvWGZd9ftl4xeGQ== +sharetribe-flex-sdk@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/sharetribe-flex-sdk/-/sharetribe-flex-sdk-1.9.0.tgz#c7af04022acb77e75e1fac2bab8a0c11154aab24" + integrity sha512-cgoswKAOZ0Zi2uFXLoo+/dpkSshELWqmLjEKaBxtJ53CzuHvaLj6gF0q+QhGFfp/BnQjMBjMyp/Box9InIaBKg== dependencies: axios "^0.19.0" js-cookie "^2.1.3" From bb9ba89344d53ecf4489e6541ce5e267655c7a7b Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Tue, 11 Feb 2020 11:16:04 +0200 Subject: [PATCH 06/16] Change `sdk.authInfo` handling Rely on the updated return value of `sdk.authInfo`. --- server/index.js | 13 +++++++------ src/ducks/Auth.duck.js | 2 +- src/ducks/Auth.test.js | 6 +++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/server/index.js b/server/index.js index ef4fc89f7..bafed3384 100644 --- a/server/index.js +++ b/server/index.js @@ -214,14 +214,15 @@ app.get('*', (req, res) => { // authentication. const token = tokenStore.getToken(); - const refreshTokenExists = !!token && !!token.refresh_token; - - if (refreshTokenExists) { - // If refresh token exists, we assume that client can handle the situation + const scopes = !!token && token.scopes; + const isAnonymous = !!scopes && scopes.length === 1 && scopes[0] === 'public-read'; + if (isAnonymous) { + res.status(401).send(html); + } else { + // If the token is associated with other than public-read scopes, we + // assume that client can handle the situation // TODO: improve by checking if the token is valid (needs an API call) res.status(200).send(html); - } else { - res.status(401).send(html); } } else if (context.forbidden) { res.status(403).send(html); diff --git a/src/ducks/Auth.duck.js b/src/ducks/Auth.duck.js index b20b4584c..f953b2590 100644 --- a/src/ducks/Auth.duck.js +++ b/src/ducks/Auth.duck.js @@ -3,7 +3,7 @@ import { clearCurrentUser, fetchCurrentUser } from './user.duck'; import { storableError } from '../util/errors'; import * as log from '../util/log'; -const authenticated = authInfo => authInfo && authInfo.grantType === 'refresh_token'; +const authenticated = authInfo => authInfo && authInfo.isAnonymous === false; // ================ Action types ================ // diff --git a/src/ducks/Auth.test.js b/src/ducks/Auth.test.js index 1d26cf2d6..9a9489560 100644 --- a/src/ducks/Auth.test.js +++ b/src/ducks/Auth.test.js @@ -166,7 +166,7 @@ describe('Auth duck', () => { }); it('should set initial state for anonymous users', () => { - const authInfoAnonymous = { grantType: 'client_credentials' }; + const authInfoAnonymous = { isAnonymous: true }; const initialState = reducer(); expect(initialState.authInfoLoaded).toEqual(false); const state = reducer(initialState, authInfoSuccess(authInfoAnonymous)); @@ -174,8 +174,8 @@ describe('Auth duck', () => { expect(state.isAuthenticated).toEqual(false); }); - it('should set initial state for unauthenticated users', () => { - const authInfoLoggedIn = { grantType: 'refresh_token' }; + it('should set initial state for authenticated users', () => { + const authInfoLoggedIn = { isAnonymous: false }; const initialState = reducer(); expect(initialState.authInfoLoaded).toEqual(false); const state = reducer(initialState, authInfoSuccess(authInfoLoggedIn)); From c014d098ea11979288df0aa9d0afd60bf564d054 Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Tue, 11 Feb 2020 11:23:01 +0200 Subject: [PATCH 07/16] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c589cbec..98da61130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ way to update this template, but currently, we follow a pattern: ## Upcoming version 2020-XX-XX +- [add] Support for logging in as a user from Console. [#1254](https://github.com/sharetribe/ftw-daily/pull/1254) - [change] Add `handlebars` 4.5.3 and `serialize-javascript` 2.1.1 to resolutions in `package.json`. [#1251](https://github.com/sharetribe/ftw-daily/pull/1251) From eedcb40874c0e9b9dbdcc0328a658b587a2703c5 Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Thu, 13 Feb 2020 10:52:50 +0200 Subject: [PATCH 08/16] Add a note about server-side endoints --- src/routeConfiguration.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/routeConfiguration.js b/src/routeConfiguration.js index a40374bea..edd60229a 100644 --- a/src/routeConfiguration.js +++ b/src/routeConfiguration.js @@ -43,6 +43,12 @@ const draftSlug = 'draft'; const RedirectToLandingPage = () => ; +// NOTE: Most server-side endpoints are prefixed with /api. Requests to those +// endpoints are indended to be handled in the server instead of the browser and +// they will not render the application. So remember to avoid routes starting +// with /api and if you encounter clashing routes see server/index.js if there's +// a conflicting route defined there. + // Our routes are exact by default. // See behaviour from Routes.js where Route is created. const routeConfiguration = () => { From dc442e99f914ab516eafab55486281c5970acba7 Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Fri, 14 Feb 2020 11:17:41 +0200 Subject: [PATCH 09/16] Add token scopes to Auth store Add `authScopes` attribute to the Auth store. It stores an array of scopes associated with the currently stored token. --- src/ducks/Auth.duck.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ducks/Auth.duck.js b/src/ducks/Auth.duck.js index f953b2590..173b5fec1 100644 --- a/src/ducks/Auth.duck.js +++ b/src/ducks/Auth.duck.js @@ -31,6 +31,9 @@ export const USER_LOGOUT = 'app/USER_LOGOUT'; const initialState = { isAuthenticated: false, + // scopes associated with current token + authScopes: [], + // auth info authInfoLoaded: false, @@ -53,7 +56,12 @@ export default function reducer(state = initialState, action = {}) { case AUTH_INFO_REQUEST: return state; case AUTH_INFO_SUCCESS: - return { ...state, authInfoLoaded: true, isAuthenticated: authenticated(payload) }; + return { + ...state, + authInfoLoaded: true, + isAuthenticated: authenticated(payload), + authScopes: payload.scopes, + }; case LOGIN_REQUEST: return { @@ -71,7 +79,7 @@ export default function reducer(state = initialState, action = {}) { case LOGOUT_REQUEST: return { ...state, logoutInProgress: true, loginError: null, logoutError: null }; case LOGOUT_SUCCESS: - return { ...state, logoutInProgress: false, isAuthenticated: false }; + return { ...state, logoutInProgress: false, isAuthenticated: false, authScopes: [] }; case LOGOUT_ERROR: return { ...state, logoutInProgress: false, logoutError: payload }; From 650dfcf4d7c6e80dc51c866a93585604699d16d6 Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Fri, 14 Feb 2020 14:29:00 +0200 Subject: [PATCH 10/16] Add a banner indicating limited access login Show a banner if a user is logged in with an access token that has `user:limited` scope. --- CHANGELOG.md | 5 +- .../LimitedAccessBanner.css | 31 +++++++++ .../LimitedAccessBanner.js | 68 +++++++++++++++++++ src/components/Topbar/Topbar.js | 13 +++- src/components/index.js | 1 + .../TopbarContainer/TopbarContainer.js | 9 ++- src/translations/en.json | 2 + 7 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 src/components/LimitedAccessBanner/LimitedAccessBanner.css create mode 100644 src/components/LimitedAccessBanner/LimitedAccessBanner.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 98da61130..5053e9c57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,10 @@ way to update this template, but currently, we follow a pattern: ## Upcoming version 2020-XX-XX -- [add] Support for logging in as a user from Console. [#1254](https://github.com/sharetribe/ftw-daily/pull/1254) +- [add] Show a banner when a user is logged in with limited access. + [#1259](https://github.com/sharetribe/ftw-daily/pull/1259) +- [add] Support for logging in as a user from Console. + [#1254](https://github.com/sharetribe/ftw-daily/pull/1254) - [change] Add `handlebars` 4.5.3 and `serialize-javascript` 2.1.1 to resolutions in `package.json`. [#1251](https://github.com/sharetribe/ftw-daily/pull/1251) diff --git a/src/components/LimitedAccessBanner/LimitedAccessBanner.css b/src/components/LimitedAccessBanner/LimitedAccessBanner.css new file mode 100644 index 000000000..3edc8658a --- /dev/null +++ b/src/components/LimitedAccessBanner/LimitedAccessBanner.css @@ -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; + } +} diff --git a/src/components/LimitedAccessBanner/LimitedAccessBanner.js b/src/components/LimitedAccessBanner/LimitedAccessBanner.js new file mode 100644 index 000000000..8057cf1b2 --- /dev/null +++ b/src/components/LimitedAccessBanner/LimitedAccessBanner.js @@ -0,0 +1,68 @@ +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.length === 1 && + authScopes[0] === 'user:limited' && + !disabledPages.includes(currentPage); + + const { firstName, lastName } = user.attributes.profile; + + return showBanner ? ( +
+

+ +

+ +
+ ) : 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; diff --git a/src/components/Topbar/Topbar.js b/src/components/Topbar/Topbar.js index 9c5f09aad..786b69ea1 100644 --- a/src/components/Topbar/Topbar.js +++ b/src/components/Topbar/Topbar.js @@ -12,6 +12,7 @@ import { createResourceLocatorString, pathByRouteName } from '../../util/routes' import { propTypes } from '../../util/types'; import { Button, + LimitedAccessBanner, Logo, Modal, ModalMissingInformation, @@ -134,6 +135,7 @@ class TopbarComponent extends Component { mobileRootClassName, mobileClassName, isAuthenticated, + authScopes, authInProgress, currentUser, currentUserHasListings, @@ -189,6 +191,13 @@ class TopbarComponent extends Component { return (
+