diff --git a/i18n/english.yml b/i18n/english.yml index b1603b944..94dcacdc9 100644 --- a/i18n/english.yml +++ b/i18n/english.yml @@ -1316,6 +1316,9 @@ components: myAccount: My account logout: Log out UserHomePage: + authDisabledInfo: >- + You are running %appTitle% without user authentication enabled. + Features such as user management, account management and feed activity watching are unavailable. createFirst: Create my first project help: content: 'A project is used to group GTFS feeds. For example, the feeds in a project may be in the same region or they may collectively define a planning scenario.' diff --git a/i18n/german.yml b/i18n/german.yml index 9a7228158..deee17c8b 100644 --- a/i18n/german.yml +++ b/i18n/german.yml @@ -1335,6 +1335,9 @@ components: logout: Abmelden myAccount: Mein Konto UserHomePage: + authDisabledInfo: >- + You are running %appTitle% without user authentication enabled. + Features such as user management, account management and feed activity watching are unavailable. createFirst: Erstelle mein erstes Projekt help: content: Ein Projekt dient dazu, GTFS-Feeds zu gruppieren. Zum Beispiel können diff --git a/i18n/polish.yml b/i18n/polish.yml index bf2938c43..1195112a5 100644 --- a/i18n/polish.yml +++ b/i18n/polish.yml @@ -1308,6 +1308,9 @@ components: logout: Log out myAccount: My account UserHomePage: + authDisabledInfo: >- + You are running %appTitle% without user authentication enabled. + Features such as user management, account management and feed activity watching are unavailable. createFirst: Create my first project help: content: A project is used to group GTFS feeds. For example, the feeds in a diff --git a/lib/admin/components/AdminPage.js b/lib/admin/components/AdminPage.js index 0206fd9b7..ce27c9c15 100644 --- a/lib/admin/components/AdminPage.js +++ b/lib/admin/components/AdminPage.js @@ -22,6 +22,7 @@ import {getComponentMessages, isModuleEnabled} from '../../common/util/config' import * as projectActions from '../../manager/actions/projects' import type {DataToolsConfig, Project} from '../../types' import type {AppState, ManagerUserState, RouterProps} from '../../types/reducers' +import { AUTH0_DISABLED } from '../../common/constants' import OrganizationList from './OrganizationList' import ServerSettings from './ServerSettings' @@ -51,8 +52,8 @@ class AdminPage extends React.Component { } = this.props // Set default path to user admin view. if (!activeComponent) browserHistory.push('/admin/users') - // Always load a fresh list of users on load. - fetchUsers() + // Always load a fresh list of users on load, if auth is not disabled. + if (!AUTH0_DISABLED) fetchUsers() // Always load projects to prevent interference with public feeds viewer // loading of projects. fetchProjects() @@ -71,7 +72,8 @@ class AdminPage extends React.Component { const {activeComponent} = this.props const restricted =

Restricted access

switch (activeComponent) { - case 'users': return + case 'users': + return AUTH0_DISABLED ? restricted : case 'organizations': if (!isApplicationAdmin || isModuleEnabled('enterprise')) return restricted else return diff --git a/lib/admin/components/ApplicationStatus.js b/lib/admin/components/ApplicationStatus.js index 54ef02cdc..cd88479be 100644 --- a/lib/admin/components/ApplicationStatus.js +++ b/lib/admin/components/ApplicationStatus.js @@ -9,6 +9,7 @@ import BootstrapTable from 'react-bootstrap-table/lib/BootstrapTable' import TableHeaderColumn from 'react-bootstrap-table/lib/TableHeaderColumn' import * as adminActions from '../actions/admin' +import { AUTH0_DISABLED } from '../../common/constants' import {getComponentMessages} from '../../common/util/config' import {formatTimestamp} from '../../common/util/date-time' import type {ServerJob} from '../../types' @@ -114,14 +115,19 @@ class ApplicationStatusView extends Component { return })} -

{this.messages('userLogs')}

- + {!AUTH0_DISABLED && ( + <> +

{this.messages('userLogs')}

+ + + )} ) } diff --git a/lib/common/components/UserButtons.js b/lib/common/components/UserButtons.js index 2749e129e..b0f4a28ba 100644 --- a/lib/common/components/UserButtons.js +++ b/lib/common/components/UserButtons.js @@ -5,8 +5,9 @@ import { Button, ButtonToolbar } from 'react-bootstrap' import { LinkContainer } from 'react-router-bootstrap' import Icon from '@conveyal/woonerf/components/icon' -import {getComponentMessages} from '../../common/util/config' -import type {ManagerUserState} from '../../types/reducers' +import { AUTH0_DISABLED } from '../constants' +import { getComponentMessages } from '../../common/util/config' +import type { ManagerUserState } from '../../types/reducers' type Props = { logout: () => any, @@ -42,13 +43,16 @@ export default class UserButtons extends Component { )} - + {/* "Log out" Button (unless auth is disabled) */} + {!AUTH0_DISABLED && ( + + )} ) } diff --git a/lib/common/constants/index.js b/lib/common/constants/index.js index 44d714301..bf1ea8f15 100644 --- a/lib/common/constants/index.js +++ b/lib/common/constants/index.js @@ -15,6 +15,7 @@ export const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID || '' export const AUTH0_CONNECTION_NAME = process.env.AUTH0_CONNECTION_NAME || '' export const AUTH0_DEFAULT_SCOPE = 'app_metadata profile email openid user_metadata' export const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN || '' +export const AUTH0_DISABLED = Boolean(process.env.DISABLE_AUTH) export const AUTO_DEPLOY_TYPES = Object.freeze({ ON_FEED_FETCH: 'ON_FEED_FETCH', diff --git a/lib/common/containers/App.js b/lib/common/containers/App.js index b3859a7ad..5ad5b2f9d 100644 --- a/lib/common/containers/App.js +++ b/lib/common/containers/App.js @@ -13,7 +13,7 @@ import MainAlertsViewer from '../../alerts/containers/MainAlertsViewer' import ActiveAlertEditor from '../../alerts/containers/ActiveAlertEditor' import Login from '../components/Login' import PageNotFound from '../components/PageNotFound' -import { AUTH0_CLIENT_ID, AUTH0_DEFAULT_SCOPE, AUTH0_DOMAIN } from '../constants' +import { AUTH0_CLIENT_ID, AUTH0_DEFAULT_SCOPE, AUTH0_DISABLED, AUTH0_DOMAIN } from '../constants' import ActiveGtfsEditor from '../../editor/containers/ActiveGtfsEditor' import ActiveFeedSourceViewer from '../../manager/containers/ActiveFeedSourceViewer' import ActiveProjectsList from '../../manager/containers/ActiveProjectsList' @@ -29,6 +29,7 @@ import type { dispatchFn, getStateFn } from '../../types/reducers' import ActiveUserRetriever from './ActiveUserRetriever' import AppInfoRetriever from './AppInfoRetriever' +import LocalUserRetriever from './LocalUserRetriever' import wrapComponentInAuthStrategy from './wrapComponentInAuthStrategy' function loginOptional (ComponentToWrap) { @@ -127,26 +128,35 @@ export default class App extends React.Component { path: '*' } ] - const routerWithAuth0 = ( - - + const appContent = ( + <> {routes.map((r, i) => ())} - + ) + const routerWithAuth0 = AUTH0_DISABLED ? <> + + {appContent} + + : ( + + + {appContent} + + ) // Initialize toast notifications. toast.configure() // Configure bugsnag if key is provided. diff --git a/lib/common/containers/LocalUserRetriever.js b/lib/common/containers/LocalUserRetriever.js new file mode 100644 index 000000000..9a21b83a7 --- /dev/null +++ b/lib/common/containers/LocalUserRetriever.js @@ -0,0 +1,60 @@ +// @flow +// $FlowFixMe useEffect not recognized by flow. +import { useEffect } from 'react' +import { connect } from 'react-redux' + +import * as userActions from '../../manager/actions/user' +import { AUTH0_CLIENT_ID } from '../constants' + +type Props = { + receiveTokenAndProfile: typeof userActions.receiveTokenAndProfile +} + +const profile = { + app_metadata: { + 'datatools': [ + { + 'permissions': [ + { + 'type': 'administer-application' + } + ], + 'projects': [], + 'client_id': AUTH0_CLIENT_ID, + 'subscriptions': [] + } + ], + 'roles': [ + 'user' + ] + }, + // FIXME: pick a better email address for both backend and frontend. + email: 'mock@example.com', + name: 'localuser', + nickname: 'Local User', + picture: 'https://d2tyb7byn1fef9.cloudfront.net/ibi_group_black-512x512.png', + sub: 'localuser', + user_id: 'localuser', + user_metadata: {} +} + +const token = 'local-user-token' + +/** + * This component provides a user profile for configs without authentication. + */ +const LocalUserRetriever = ({ receiveTokenAndProfile }: Props) => { + // Update the user info in the redux state on initialization. + useEffect(() => { + receiveTokenAndProfile({ profile, token }) + }, []) + + // Component renders nothing. + return null +} + +const mapDispatchToProps = { + receiveTokenAndProfile: userActions.receiveTokenAndProfile +} + +export default connect(null, mapDispatchToProps)(LocalUserRetriever) diff --git a/lib/common/containers/WatchButton.js b/lib/common/containers/WatchButton.js index 86702dcfa..99c899b59 100644 --- a/lib/common/containers/WatchButton.js +++ b/lib/common/containers/WatchButton.js @@ -7,8 +7,8 @@ import {connect} from 'react-redux' import * as userActions from '../../manager/actions/user' import * as statusActions from '../../manager/actions/status' import {getComponentMessages, getConfigProperty} from '../util/config' - import type {AppState, ManagerUserState} from '../../types/reducers' +import { AUTH0_DISABLED } from '../constants' type ContainerProps = { componentClass?: string, @@ -84,9 +84,12 @@ class WatchButton extends Component { } render () { + // Do not render watch button if notifications are not enabled or if auth is disabled. + // (Notifications require an email address verified with Auth0.) + if (AUTH0_DISABLED || !getConfigProperty('application.notifications_enabled')) { + return null + } const {componentClass} = this.props - // Do not render watch button if notifications are not enabled. - if (!getConfigProperty('application.notifications_enabled')) return null switch (componentClass) { case 'menuItem': return ( diff --git a/lib/common/containers/wrapComponentInAuthStrategy.js b/lib/common/containers/wrapComponentInAuthStrategy.js index 20f72ea89..ca1007f39 100644 --- a/lib/common/containers/wrapComponentInAuthStrategy.js +++ b/lib/common/containers/wrapComponentInAuthStrategy.js @@ -9,6 +9,7 @@ import { browserHistory } from 'react-router' import {getComponentMessages} from '../util/config' import type {AppState, ManagerUserState} from '../../types/reducers' +import { AUTH0_DISABLED } from '../constants' type AuthWrapperProps = { user: ManagerUserState @@ -68,7 +69,7 @@ export default function wrapComponentInAuthStrategy ( (state: AppState) => ({user: state.user}) )(AuthStrategyWrapper) - if (requireAuth || requireAdmin) { + if (!AUTH0_DISABLED && (requireAuth || requireAdmin)) { return withAuthenticationRequired(connectedComponent) } diff --git a/lib/manager/components/FeedSourceViewer.js b/lib/manager/components/FeedSourceViewer.js index c7fdfaf96..707db7dde 100644 --- a/lib/manager/components/FeedSourceViewer.js +++ b/lib/manager/components/FeedSourceViewer.js @@ -14,17 +14,17 @@ import * as snapshotActions from '../../editor/actions/snapshots' import * as gtfsPlusActions from '../../gtfsplus/actions/gtfsplus' import ManagerPage from '../../common/components/ManagerPage' import {getComponentMessages, isModuleEnabled} from '../../common/util/config' -import ManagerHeader from './ManagerHeader' import ActiveFeedVersionNavigator from '../containers/ActiveFeedVersionNavigator' -import FeedSourceSettings from './FeedSourceSettings' -import NotesViewer from './NotesViewer' import ActiveEditorFeedSourcePanel from '../../editor/containers/ActiveEditorFeedSourcePanel' import {isEditingDisabled} from '../util' - import type {Props as ContainerProps} from '../containers/ActiveFeedSourceViewer' import type {Feed, Note, Project} from '../../types' import type {ManagerUserState} from '../../types/reducers' +import NotesViewer from './NotesViewer' +import FeedSourceSettings from './FeedSourceSettings' +import ManagerHeader from './ManagerHeader' + type Props = ContainerProps & { activeComponent: string, activeSubComponent: string, diff --git a/lib/manager/components/UserHomePage.js b/lib/manager/components/UserHomePage.js index 951177f27..e09afac1b 100644 --- a/lib/manager/components/UserHomePage.js +++ b/lib/manager/components/UserHomePage.js @@ -3,14 +3,14 @@ import { Auth0ContextInterface } from '@auth0/auth0-react' import Icon from '@conveyal/woonerf/components/icon' import React, {Component} from 'react' -import {Grid, Row, Col, Button, ButtonToolbar, Jumbotron} from 'react-bootstrap' +import { Alert, Button, ButtonToolbar, Col, Grid, Jumbotron, Row } from 'react-bootstrap' import objectPath from 'object-path' import * as feedsActions from '../actions/feeds' import * as userActions from '../actions/user' import * as visibilityFilterActions from '../actions/visibilityFilter' import ManagerPage from '../../common/components/ManagerPage' -import { DEFAULT_DESCRIPTION, DEFAULT_TITLE } from '../../common/constants' +import { AUTH0_DISABLED, DEFAULT_DESCRIPTION, DEFAULT_TITLE } from '../../common/constants' import {getConfigProperty, getComponentMessages} from '../../common/util/config' import {defaultSorter} from '../../common/util/util' import type {Props as ContainerProps} from '../containers/ActiveUserHomePage' @@ -85,6 +85,7 @@ export default class UserHomePage extends Component { } = this.props const visibleProjects = projects.sort(defaultSorter) const activeProject = project + const appTitle = getConfigProperty('application.title') || 'datatools' return ( { {/* Top Welcome Box */} -

{this.messages('welcomeTo')} {getConfigProperty('application.title') || DEFAULT_TITLE}!

+

{this.messages('welcomeTo')} {appTitle || DEFAULT_TITLE}!

{getConfigProperty('application.description') || DEFAULT_DESCRIPTION}

+ {/* Info banner shown if auth is disabled. */} + {AUTH0_DISABLED && ( + + + {this.messages('authDisabledInfo').replace('%appTitle%', appTitle)} + + )} {/* Recent Activity List */}

{this.messages('recentActivity')} diff --git a/lib/manager/containers/FeedSourceTable.js b/lib/manager/containers/FeedSourceTable.js index b796346cd..872c515f7 100644 --- a/lib/manager/containers/FeedSourceTable.js +++ b/lib/manager/containers/FeedSourceTable.js @@ -4,7 +4,6 @@ import {connect} from 'react-redux' import FeedSourceTable from '../components/FeedSourceTable' import {getFilteredFeeds} from '../util' - import type {Project} from '../../types' import type {AppState} from '../../types/reducers' diff --git a/lib/manager/util/index.js b/lib/manager/util/index.js index 28c1309a5..f43b1ca22 100644 --- a/lib/manager/util/index.js +++ b/lib/manager/util/index.js @@ -245,12 +245,12 @@ export function isEditingDisabled ( feedSource: Feed, project: ?Project ): boolean { - // If any of the args or null, return ( + // If any of the args are null... !user || !feedSource || !project || - // or the user does not have permission, editing is disabled. + // ...or the user does not have permission, editing is disabled. !user.permissions || !user.permissions.hasFeedPermission( project.organizationId, diff --git a/lib/public/components/PublicHeader.js b/lib/public/components/PublicHeader.js index d4474e13b..92605155c 100644 --- a/lib/public/components/PublicHeader.js +++ b/lib/public/components/PublicHeader.js @@ -2,12 +2,13 @@ import type { Auth0ContextInterface } from '@auth0/auth0-react' import Icon from '@conveyal/woonerf/components/icon' -import React, {Component} from 'react' -import {Grid, Row, Col, Button, Glyphicon, ButtonToolbar} from 'react-bootstrap' -import {LinkContainer} from 'react-router-bootstrap' +import React, { Component } from 'react' +import { Button, ButtonToolbar, Col, Glyphicon, Grid, Row } from 'react-bootstrap' +import { LinkContainer } from 'react-router-bootstrap' import * as userActions from '../../manager/actions/user' -import type {Props as ContainerProps} from '../containers/ActivePublicHeader' +import type { Props as ContainerProps } from '../containers/ActivePublicHeader' +import { AUTH0_DISABLED } from '../../common/constants' type Props = ContainerProps & { auth0: Auth0ContextInterface, @@ -17,21 +18,7 @@ type Props = ContainerProps & { username: ?string } -type State = { - showLogin: boolean -} - -export default class PublicHeader extends Component { - state = { showLogin: false } - - _onLoginClick = () => { - this.setState({ showLogin: true }) - } - - _onLoginHide = () => { - this.setState({ showLogin: false }) - } - +export default class PublicHeader extends Component { _onLogoutClick = () => { this.props.logout(this.props.auth0) } @@ -73,14 +60,12 @@ export default class PublicHeader extends Component { ) } - {/* "Log out" Button */} - {username - ? ( - - ) - : null} + {/* "Log out" Button (unless auth is disabled) */} + {username && !AUTH0_DISABLED && ( + + )} diff --git a/lib/public/components/UserAccount.js b/lib/public/components/UserAccount.js index f893a2e2d..8cd472350 100644 --- a/lib/public/components/UserAccount.js +++ b/lib/public/components/UserAccount.js @@ -7,6 +7,7 @@ import {browserHistory, Link} from 'react-router' import {LinkContainer} from 'react-router-bootstrap' import ManagerPage from '../../common/components/ManagerPage' +import { AUTH0_DISABLED } from '../../common/constants' import {getComponentMessages, getConfigProperty, isModuleEnabled} from '../../common/util/config' import * as deploymentsActions from '../../manager/actions/deployments' import * as feedsActions from '../../manager/actions/feeds' @@ -100,48 +101,52 @@ export default class UserAccount extends Component {
Profile Information - - - Email address -
{profile.email}
-
- {/* Display account type if two or more are configured and if the relevant text is available. - (or the account type from the user profile does not match the one(s) configured). */} - {((Object.keys(accountTypes).length > 1 && displayedAccountType) || accountTypeIsUnknown) && ( + {!AUTH0_DISABLED && ( + // If auth is disabled, simply show nothing under the Profile pane, + // but keep the Profile pane visible because it is the default one for Settings. + - Account type -
- {displayedAccountType} - {/* Show account terms URL only if one has been configured. */} - {accountTermsUrl && ( - - {' - '} - Terms and conditions - - )} -
+ Email address +
{profile.email}
- )} - -

Avatar

- - Profile - Change on gravatar.com - -
- -

Password

- -
-
+ {/* Display account type if two or more are configured and if the relevant text is available + (or the account type from the user profile does not match the one(s) configured). */} + {((Object.keys(accountTypes).length > 1 && displayedAccountType) || accountTypeIsUnknown) && ( + + Account type +
+ {displayedAccountType} + {/* Show account terms URL only if one has been configured. */} + {accountTermsUrl && ( + + {' - '} + Terms and conditions + + )} +
+
+ )} + +

Avatar

+ + Profile + Change on gravatar.com + +
+ +

Password

+ +
+
+ )}
)