Skip to content

Commit

Permalink
Move settings to config; enable separate server for OAuth
Browse files Browse the repository at this point in the history
  • Loading branch information
osteele committed May 26, 2018
1 parent de34f1b commit 5b14b23
Show file tree
Hide file tree
Showing 13 changed files with 113 additions and 72 deletions.
2 changes: 1 addition & 1 deletion src/__test__/__snapshots__/sidebar.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ exports[`Sidebar renders sign in 1`] = `
</p>
<p>
<a
href="http://abe_url/oauth/authorize?redirect_uri=about%3Ablank&response_type=token&client_id=$abe-client-id"
href="http://abe_url/oauth/authorize?client_id=$abe-client-id&redirect_uri=about%3Ablank&response_type=token&scope=create%3Aevents%20create%3Aics%20create%3Aprotected_events%20delete%3Aevents%20delete%3Aprotected_events%20edit%3Aevents%20edit%3Aprotected_events%20read%3Aall_events%20read%3Alabels"
>
Sign in
</a>
Expand Down
4 changes: 2 additions & 2 deletions src/__test__/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class LocalStorageMock {
}
}

global.abe_url = 'http://abe_url';
global.localStorage = new LocalStorageMock();

process.env.ABE_CLIENT_ID = '$abe-client-id';
process.env.ABE_URL = 'http://abe_url';
process.env.CLIENT_ID = '$abe-client-id';
10 changes: 2 additions & 8 deletions src/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,10 @@ import LabelsContainer from './containers/labels-container';
import SidebarContainer from './containers/sidebar-container';
import SubscriptionContainer from './containers/subscription-container';
import { withAccountInfo } from './containers/with-server-data';
import { fetchLabels, fetchUser, toggleSidebarCollapsed } from './data/actions';
import { fetchLabels, fetchAccessInfo, toggleSidebarCollapsed } from './data/actions';
import { initializeAccessToken } from './data/auth';
import setupStore from './data/setup-store';

// Remove the trailing slash, if present
//
// TODO: change this from a global variable to an API Client that's supplied via
// the provider pattern.
window.abe_url = process.env.ABE_URL.replace(/\/$/, '');

// React Router (with Redux middleware)
const history = createHistory();

Expand All @@ -30,7 +24,7 @@ const store = setupStore(history);

initializeAccessToken();

store.dispatch(fetchUser());
store.dispatch(fetchAccessInfo());
store.dispatch(fetchLabels());

const App = () => (
Expand Down
57 changes: 31 additions & 26 deletions src/data/actions.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// This file contains a bunch of Redux actions

import axios from 'axios';
import _ from 'lodash';
import moment from 'moment';
import ReactGA from 'react-ga';
import { push } from 'react-router-redux';
import { setAccessTokenFromResponse } from './auth';
import { getAccessToken, setAccessTokenFromResponse } from './auth';
import { decodeEvent } from './encoding';
import { API_SERVER_URL, TOKEN_INFO_ENDPOINT } from './settings';

/* eslint-disable max-len */
export const ActionTypes = {
Expand All @@ -25,7 +27,7 @@ export const ActionTypes = {
SET_FILTER_LABEL_SELECTED: 'SET_FILTER_LABEL_SELECTED', // Sets whether or not a specific label is selected as part
// of the event filter
SET_VIEW_MODE: 'SET_VIEW_MODE', // Sets which view mode (month, week, day, etc) the calendar is in
SET_USER: 'SET_USER',
SET_ACCESS_INFO: 'SET_ACCESS_INFO',
// Event data
SET_CURRENT_EVENT: 'SET_CURRENT_EVENT', // Keeps track of the data for the event currently being viewed or edited
FETCH_EVENTS_IF_NEEDED: 'FETCH_EVENTS_IF_NEEDED', // Triggers FETCH_EVENTS if no event data is loaded
Expand Down Expand Up @@ -231,24 +233,27 @@ export function setViewMode(mode) {
/**
* Performs a server request to refresh the user info.
*/
export function fetchUser() {
return dispatch =>
axios
.get(`${window.abe_url}/user/`)
.then(setAccessTokenFromResponse)
.then(response => response.data, error => dispatch(displayError(error)))
.then(user => dispatch(setUser(user)));
}

/**
* Updates the user in the Redux store.
*/
export function setUser(user) {
const data = {
authenticated: user.authenticated,
scope: new Set(user.scope),
};
return { type: ActionTypes.SET_USER, data };
export function fetchAccessInfo() {
function setAccessInfo(user) {
const token = user.token || {};
// TODO: remove support for string array scope
const { scope } = user;
const scopes = _.isString(scope) ? user.scope.split(' ') : scope;
const data = {
authenticated: token.active,
scope: new Set(scopes),
};
return { type: ActionTypes.SET_ACCESS_INFO, data };
}
const token = getAccessToken();
return token
? dispatch =>
axios
.get(TOKEN_INFO_ENDPOINT, { params: { token } })
.then(setAccessTokenFromResponse)
.then(response => response.data, error => dispatch(displayError(error)))
.then(user => dispatch(setAccessInfo(user)))
: dispatch => dispatch(setAccessInfo({}));
}

// ########## Begin Event Data Actions ########## //
Expand Down Expand Up @@ -276,8 +281,8 @@ export function setCurrentEventById(id, recId) {
} else {
// We don't have the data, so request it from the server
const url = recId
? `${window.abe_url}/events/${id}/${recId}`
: `${window.abe_url}/events/${id}`;
? `${API_SERVER_URL}/events/${id}/${recId}`
: `${API_SERVER_URL}/events/${id}`;
axios
.get(url)
.then(response => dispatch(setCurrentEventData(response.data)))
Expand Down Expand Up @@ -308,7 +313,7 @@ export function refreshEvents(start, end) {
const startString = `${start.year()}-${start.month() + 1}-${start.date()}`;
const endString = `${end.year()}-${end.month() + 1}-${end.date()}`;
return axios
.get(`${window.abe_url}/events/?start=${startString}&end=${endString}`)
.get(`${API_SERVER_URL}/events/?start=${startString}&end=${endString}`)
.then(response => response.data, error => dispatch(displayError(error)))
.then((data) => {
const events = data.map(decodeEvent);
Expand Down Expand Up @@ -378,7 +383,7 @@ export function deleteCurrentEvent() {
const store = getStore();
const event = store.events.current;
axios
.delete(`${window.abe_url}/events/${event.id || event.sid}`)
.delete(`${API_SERVER_URL}/events/${event.id || event.sid}`)
.then(() => dispatch(eventDeletedSuccessfully(event.id || event.sid)))
.catch(response => eventDeleteFailed(event, response));
};
Expand Down Expand Up @@ -511,7 +516,7 @@ export function refreshLabelsIfNeeded() {
export function fetchLabels() {
return dispatch =>
axios
.get(`${window.abe_url}/labels/`)
.get(`${API_SERVER_URL}/labels/`)
.then(response => response.data, error => dispatch(displayError(error)))
.then(labels => dispatch(setLabels(labels)));
}
Expand Down Expand Up @@ -573,7 +578,7 @@ export function updateLabel(data) {
return () => {
// TODO: update model on success
axios
.post(`${window.abe_url}/labels/${data.id}`, data)
.post(`${API_SERVER_URL}/labels/${data.id}`, data)
.catch(error => alert(`Update label failed:\n${error}`));
};
}
Expand Down
54 changes: 42 additions & 12 deletions src/data/auth.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import axios from 'axios';
import { CLIENT_ID, OAUTH_AUTH_ENDPOINT } from './settings';

// localStorage key
const ACCESS_TOKEN_KEY = 'abeAccessToken';

export const Scopes = new Set([
'create:events',
'create:ics',
'create:protected_events',
'delete:events',
'delete:protected_events',
'edit:events',
'edit:protected_events',
'read:all_events',
'read:labels',
]);

export function authorizationUrl(redirectUri) {
const oauthBaseUrl = `${window.abe_url}/oauth/authorize`;
const clientId = process.env.ABE_CLIENT_ID;
let url = `${oauthBaseUrl}?redirect_uri=${encodeURIComponent(redirectUri)}`;
url += '&response_type=token';
if (clientId) {
url += `&client_id=${clientId}`;
}
const url = [
OAUTH_AUTH_ENDPOINT,
`?client_id=${CLIENT_ID}`,
`&redirect_uri=${encodeURIComponent(redirectUri)}`,
'&response_type=token',
`&scope=${encodeURIComponent([...Scopes].join(' '))}`,
].join('');
return url;
}

Expand All @@ -18,33 +32,49 @@ export function clearAccessToken() {
axios.defaults.headers.common.Authorization = null;
}

export function getAccessToken() {
return localStorage[ACCESS_TOKEN_KEY];
}

// Remember the access token into localStorage (where it's available the next
// time the page is visited), and also present it in subsequent API requests.
export function setAccessToken(accessToken) {
localStorage[ACCESS_TOKEN_KEY] = accessToken;
axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
}

// Returns true if the user can sign out.
export function canSignOut() {
return Boolean(localStorage[ACCESS_TOKEN_KEY]);
return Boolean(getAccessToken());
}

// Remove the OAuth flow parameters from the URL fragment
export function removeOauthFragments(url) {
return url
.replace(/([#&])(access_token|expires_in|state|token_type)=[^&]*/g, '$1')
.replace(/([#&])(access_token|expires_in|nonce|state|token_type)=[^&]*/g, '$1')
.replace(/([#&])&+/, '$1')
.replace(/[#&]$/, '');
}

// Called on startup. Reads the access token from localStorage or from the URL
// fragment (for an OAuth implicit grant callback), and sanitizes the URL in
// the latter case.
export function initializeAccessToken() {
const token = localStorage[ACCESS_TOKEN_KEY];
let token = localStorage[ACCESS_TOKEN_KEY];
const match = document.location.hash.match(/[#&]access_token=([^&]*)/);
// If there's a token in the URL, this replaces any locally-stored token.
if (match) {
setAccessToken(decodeURIComponent(match[1]));
token = decodeURIComponent(match[1]);
window.history.replaceState({}, document.title, removeOauthFragments(window.location.href));
} else if (token) {
}
if (token) {
setAccessToken(token);
}
}

// Set the token from the 'Access-Token' HTTP response header.
//
// TODO: I think this is obsolete — ows 2018-05-26
export function setAccessTokenFromResponse(response) {
const token = response.headers['access-token'];
if (token) {
Expand Down
2 changes: 1 addition & 1 deletion src/data/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function general(state = {}, action) {

export function user(state = {}, action) {
switch (action.type) {
case ActionTypes.SET_USER:
case ActionTypes.SET_ACCESS_INFO:
return action.data;
default:
return state;
Expand Down
13 changes: 13 additions & 0 deletions src/data/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const API_SERVER_URL = process.env.ABE_URL.replace(/\/$/, '');
export const OAUTH_AUTH_ENDPOINT =
process.env.OAUTH_AUTH_ENDPOINT || `${API_SERVER_URL}/oauth/authorize`;
// export const TOKEN_INFO_ENDPOINT = process.env.TOKEN_INFO_ENDPOINT || `${API_SERVER_URL}/user/`;
export const TOKEN_INFO_ENDPOINT = `${API_SERVER_URL}/oauth/introspect`;
export const CLIENT_ID = process.env.CLIENT_ID || '0';

export const debug = process.env.DEBUG || false;
export const googleAnalyticsId = process.env.GA_ID;

// TODO: Replace these with some user-configurable option
export const dayEndHour = process.env.DAY_END_HOUR || 24;
export const dayStartHour = process.env.DAY_START_HOUR || 8;
7 changes: 1 addition & 6 deletions src/data/setup-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,9 @@ import { applyMiddleware, combineReducers, createStore } from 'redux';
import thunkMiddleware from 'redux-thunk';
import * as reducers from './reducers';
import SidebarMode from './sidebar-modes';
import { debug, googleAnalyticsId, dayStartHour, dayEndHour } from './settings';

export default function setupStore(history) {
const debug = process.env.DEBUG || false;
const googleAnalyticsId = process.env.GA_ID;
// TODO: Replace these with some user-configurable option
const dayStartHour = process.env.DAY_START_HOUR || 8;
const dayEndHour = process.env.DAY_END_HOUR || 24;

const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
const initialState = {
general: {
Expand Down
11 changes: 6 additions & 5 deletions src/pages/add-edit/add-edit-page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import LocationField from './location-field';
import RecurrenceSelector from './recurrence-selector';
import SaveCancelButtons from './save-cancel-buttons';
import EventVisibilitySelector from './visibility-selector';
import { API_SERVER_URL } from '../../data/settings';

export default class AddEditEventPage extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -58,7 +59,7 @@ export default class AddEditEventPage extends React.Component {
// Editing a specific recurrence in a series of events
// Get the series data so we know how this event differs from the rest in the series
axios
.get(`${window.abe_url}/events/${props.match.params.id}`)
.get(`${API_SERVER_URL}/events/${props.match.params.id}`)
.then(this.receivedSuccessfulSeriesDataResponse);
// TODO: Handle an unsuccessful response
}
Expand Down Expand Up @@ -154,17 +155,17 @@ export default class AddEditEventPage extends React.Component {
}
});

url = `${window.abe_url}/events/${eventData.id || eventData.sid}/${
url = `${API_SERVER_URL}/events/${eventData.id || eventData.sid}/${
this.props.match.params.recId
}`;
} else {
url = `${window.abe_url}/events/${eventData.id || eventData.sid}`;
url = `${API_SERVER_URL}/events/${eventData.id || eventData.sid}`;
}
delete eventData.color; // Don't send the color used for rendering the calendar
requestMethod = axios.put;
} else {
// We're adding a new event
url = `${window.abe_url}/events/`;
url = `${API_SERVER_URL}/events/`;
requestMethod = axios.post;
}

Expand Down Expand Up @@ -241,7 +242,7 @@ export default class AddEditEventPage extends React.Component {

const { scope } = this.props.user;
const editingExisting = this.state.eventData.id || this.state.eventData.sid;
const requiredScope = editingExisting ? 'create:protected_events' : 'edit:protected_events';
const requiredScope = 'edit:protected_events';
const pageTitle = editingExisting ? 'Edit Event' : 'Add Event';
const submitButtonText = editingExisting ? 'Update Event' : 'Add Event';
const formUrl = 'https://goo.gl/forms/2cqVijokICZ5S20R2';
Expand Down
7 changes: 4 additions & 3 deletions src/pages/import/import.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// This component allows the user to import a calendar from an ICS feed

import * as React from 'react';
import axios from 'axios';
import SidebarModes from '../../data/sidebar-modes';
import * as React from 'react';
import TagPane from '../../components/label-pane';
import MenuIconButton from '../../components/menu-icon-button';
import SidebarModes from '../../data/sidebar-modes';
import { API_SERVER_URL } from '../../data/settings';

export default class ImportPage extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -36,7 +37,7 @@ export default class ImportPage extends React.Component {

submitICS = () => {
axios
.post(`${window.abe_url}/ics/`, this.state.importData)
.post(`${API_SERVER_URL}/ics/`, this.state.importData)
.then(
response => this.props.importSuccess(response, this.state.importData),
(jqXHR, textStatus, errorThrown) => this.props.importFailed(errorThrown, jqXHR.message),
Expand Down

0 comments on commit 5b14b23

Please sign in to comment.