From e89ca2c7fc43a95b0fb7a5241eae89387f232ecf Mon Sep 17 00:00:00 2001 From: Artur Dudnik Date: Wed, 8 Jan 2020 22:44:20 +0300 Subject: [PATCH] add auth form #67 --- src/client/client.js | 2 +- src/client/components/AuthForm.js | 113 ++++++ src/client/components/FilterView/OrgField.js | 6 +- src/client/components/Viewport.js | 5 +- src/client/components/WrappedViewport.js | 35 +- src/client/main.js | 4 +- src/client/reducer/auth/index.js | 135 ++++++++ src/client/reducer/dashboard/index.js | 346 +++++++++---------- src/client/reducer/index.js | 3 +- src/client/reducer/state.js | 4 +- src/client/reducer/types.js | 6 + src/client/storage.js | 40 ++- src/client/utils/request.js | 13 + src/server/libs/utils.js | 22 +- src/server/models/Device.js | 14 +- src/server/models/Location.js | 33 +- src/server/models/Org.js | 11 +- src/server/routes/site-api.js | 55 +-- webpack.config.js | 4 +- 19 files changed, 595 insertions(+), 256 deletions(-) create mode 100644 src/client/components/AuthForm.js create mode 100644 src/client/reducer/auth/index.js create mode 100644 src/client/reducer/types.js create mode 100644 src/client/utils/request.js diff --git a/src/client/client.js b/src/client/client.js index 71d6959..88cefa7 100644 --- a/src/client/client.js +++ b/src/client/client.js @@ -6,7 +6,7 @@ import { Switch, } from 'react-router-dom'; -import WrappedViewport from './components/WrappedViewport'; +import WrappedViewport from 'components/WrappedViewport'; const App = ({ store }) => ( diff --git a/src/client/components/AuthForm.js b/src/client/components/AuthForm.js new file mode 100644 index 0000000..1689758 --- /dev/null +++ b/src/client/components/AuthForm.js @@ -0,0 +1,113 @@ +// @flow + +import React, { useState, useCallback } from 'react'; +import { connect } from 'react-redux'; +import Avatar from '@material-ui/core/Avatar'; +import Button from '@material-ui/core/Button'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import TextField from '@material-ui/core/TextField'; +import LockOutlinedIcon from '@material-ui/icons/LockOutlined'; +import Typography from '@material-ui/core/Typography'; +import { makeStyles, type Theme } from '@material-ui/core/styles'; +import Container from '@material-ui/core/Container'; + +import { + checkAuth, + type AuthParams, +} from 'reducer/auth'; + +import { type MaterialInputElement } from 'reducer/dashboard'; + +type DispatchProps = {| + handleSubmit: (params: AuthParams) => any, +|}; + +const useStyles = makeStyles((theme: Theme) => ({ + paper: { + marginTop: theme.spacing(8), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.secondary.main, + }, + form: { + width: '100%', // Fix IE 11 issue. + marginTop: theme.spacing(1), + }, + submit: { margin: theme.spacing(3, 0, 2) }, +})); + +const AuthForm = ({ handleCheckAuth }: DispatchProps) => { + const classes = useStyles(); + const [values, setValues] = useState({ login: '', password: '' }); + const handleInputChange = useCallback((event: MaterialInputElement) => { + const { target } = event; + const value = target.type === 'checkbox' ? target.checked : target.value; + const { name } = target; + + setValues({ + ...values, + [name]: value, + }); + }); + const handleSubmit = useCallback((event: Event) => { + event.preventDefault(); + handleCheckAuth(values); + }, values); + + return ( + + +
+ + + + + Sign in + +
+ + + + +
+
+ ); +}; + +const mapDispatchToProps: DispatchProps = { handleCheckAuth: checkAuth }; + +export default connect(undefined, mapDispatchToProps)(AuthForm); diff --git a/src/client/components/FilterView/OrgField.js b/src/client/components/FilterView/OrgField.js index 58f3d9d..7bda6a1 100644 --- a/src/client/components/FilterView/OrgField.js +++ b/src/client/components/FilterView/OrgField.js @@ -62,7 +62,7 @@ const styles = (theme: any) => ({ }, }); -const OrgField = withStyles(styles)((props: Props) => { +const OrgField = (props: Props) => { const { onChange, value, source: s, fullScreen, classes, } = props; @@ -221,6 +221,6 @@ const OrgField = withStyles(styles)((props: Props) => { , ]; -}); +}; -export default OrgField; +export default withStyles(styles)(OrgField); diff --git a/src/client/components/Viewport.js b/src/client/components/Viewport.js index 1884df3..a51b551 100644 --- a/src/client/components/Viewport.js +++ b/src/client/components/Viewport.js @@ -27,8 +27,8 @@ import TooManyPointsWarning from './TooManyPointsWarning'; type StateProps = {| isLocationSelected: boolean, - activeTabIndex: 0 | 1, - location: ?Location, + activeTabIndex: 0 | 1, + location: ?Location, |}; type DispatchProps = {| onChangeActiveTab: (tab: TabType) => any, @@ -113,4 +113,5 @@ const mapStateToProps = (state: GlobalState): StateProps => ({ location: getLocation(state), }); const mapDispatchToProps: DispatchProps = { onChangeActiveTab: changeActiveTab }; + export default connect(mapStateToProps, mapDispatchToProps)(Viewport); diff --git a/src/client/components/WrappedViewport.js b/src/client/components/WrappedViewport.js index 3d5a918..08c611b 100644 --- a/src/client/components/WrappedViewport.js +++ b/src/client/components/WrappedViewport.js @@ -1,15 +1,42 @@ +// @flow import React from 'react'; +import { connect } from 'react-redux'; +import type { GlobalState } from 'reducer/state'; import { loadInitialData } from 'reducer/dashboard'; +import { showAuthDialog } from 'reducer/auth'; import store from '../store'; import Viewport from './Viewport'; +import AuthForm from './AuthForm'; +type StateProps = {| + org: string, + match: { params?: { token: string } }, +|}; -const WrappedViewport = ({ match }) => { - store.dispatch(loadInitialData(match.params.token)); - return ; +const WrappedViewport = ({ + hasData, + match, + org, +}: StateProps) => { + const { token } = match.params; + const hasToken = !!org || !!token || !!process.env.SHARED_DASHBOARD; + const action = !hasToken + ? showAuthDialog() + : loadInitialData(token); + + !hasData && store.dispatch(action); + + return hasToken + ? + : ; }; -export default WrappedViewport; +const mapStateToProps = (state: GlobalState): StateProps => ({ + org: state.auth.org, + hasData: state.dashboard.hasData, +}); + +export default connect(mapStateToProps)(WrappedViewport); diff --git a/src/client/main.js b/src/client/main.js index 99695aa..dfa0a81 100644 --- a/src/client/main.js +++ b/src/client/main.js @@ -1,8 +1,8 @@ import React from 'react'; import { render } from 'react-dom'; -import App from './client'; -import store from './store'; +import App from 'client'; +import store from 'store'; // Detect users incorrectly hitting /locations/username instead of /username. // It seems people think because the plugin is POSTing -> /location/username that they must diff --git a/src/client/reducer/auth/index.js b/src/client/reducer/auth/index.js new file mode 100644 index 0000000..b94e968 --- /dev/null +++ b/src/client/reducer/auth/index.js @@ -0,0 +1,135 @@ +// @flow +/* eslint-disable no-console */ +import cloneState from 'utils/cloneState'; + +import { + type Dispatch, + type ThunkAction, +} from 'reducer/types'; +import { loadInitialData } from 'reducer/dashboard'; + +import { API_URL } from '../../constants'; + + +type AuthPayload = { accessToken: string, org: string }; + +type SetAccessTokenAction = {| + type: 'auth/SET_ACCESS_TOKEN', + value: AuthPayload, +|}; + +type SetAuthModalOpenAction = {| + type: 'auth/SET_MODAL_OPEN', + value: boolean, +|}; + +type AuthErrorAction = {| + type: 'auth/ERROR', + value: string, +|}; + +// Combining Actions + +type Action = + | SetAccessTokenAction + | CloseAuthModalAction + | AuthErrorAction; + +// ------------------------------------ +// Action Creators +// ------------------------------------ + +export const setAccessToken = (value: AuthPayload): SetAccessTokenAction => ({ type: 'auth/SET_ACCESS_TOKEN', value }); + +export const setAuthModalOpen = (value: boolean): SetAuthModalOpenAction => ({ type: 'auth/SET_MODAL_OPEN', value }); + +export const setAuthError = (value: string): AuthErrorAction => ({ type: 'auth/ERROR', value }); + +// ------------------------------------ +// Thunk Actions +// ------------------------------------ + +export const showAuthDialog = + (): ThunkAction => async (dispatch: Dispatch): Promise => { + await dispatch(setAuthError('')); + await dispatch(setAuthModalOpen(true)); + }; + +export const checkAuth = + ({ login, password }: AuthParams): ThunkAction => async (dispatch: Dispatch): Promise => { + try { + const response = await fetch( + `${API_URL}/auth`, + { + method: 'post', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ login, password }), + }, + ); + const { + access_token: accessToken, + org, + error, + } = await response.json(); + + if (accessToken) { + await dispatch(setAccessToken({ accessToken, org })); + await dispatch(setAuthModalOpen(false)); + await dispatch(setAuthError('')); + // setAuth({ org, accessToken }); + await dispatch(loadInitialData(org)); + } else { + await dispatch(setAuthError(error)); + } + } catch (e) { + console.error('checkAuth', e); + } + }; +// ------------------------------------ +// Action Handlers +// ------------------------------------ +const setAccessTokenHandler = + (state: AuthState, action: SetAccessTokenAction): AuthState => cloneState( + state, + action.value, + ); + +const setAuthModalOpenHandler = + (state: AuthState, action: SetAuthModalOpenAction): AuthState => cloneState( + state, + { modal: action.value }, + ); + +const setAuthErrorHandler = + (state: AuthState, action: AuthErrorAction): AuthState => cloneState( + state, + { error: action.value }, + ); + +// ------------------------------------ +// Initial State +// ------------------------------------ + +const initialState: AuthState = { + org: '', + error: '', + accessToken: '', +}; + +// ------------------------------------ +// Reducer +// ------------------------------------ +export default function spotsReducer (state: AuthState = initialState, action: Action): DashboardState { + switch (action.type) { + case 'auth/SET_ACCESS_TOKEN': + return setAccessTokenHandler(state, action); + case 'auth/SET_MODAL_OPEN': + return setAuthModalOpenHandler(state, action); + case 'auth/ERROR': + return setAuthErrorHandler(state, action); + default: + // eslint-disable-next-line no-unused-expressions + (action: empty); + return state; + } +} diff --git a/src/client/reducer/dashboard/index.js b/src/client/reducer/dashboard/index.js index 2c113f2..f370af4 100644 --- a/src/client/reducer/dashboard/index.js +++ b/src/client/reducer/dashboard/index.js @@ -1,5 +1,5 @@ -/* eslint-disable no-console */ // @flow +/* eslint-disable no-console */ import qs from 'querystring'; import isEqual from 'lodash/isEqual'; @@ -7,10 +7,20 @@ import { fitBoundsBus, scrollToRowBus, changeTabBus, } from 'globalBus'; import { - setSettings, getSettings, getUrlSettings, setUrlSettings, type StoredSettings, + setSettings, + getSettings, + getUrlSettings, + setUrlSettings, + type StoredSettings, } from 'storage'; import GA from 'utils/GA'; -import { type GlobalState, type Tab } from 'reducer/state'; +import { makeHeaders } from 'utils/request'; +import { type Tab } from 'reducer/state'; +import { + type GetState, + type Dispatch, + type ThunkAction, +} from 'reducer/types'; import cloneState from 'utils/cloneState'; import { API_URL } from '../../constants'; @@ -30,6 +40,7 @@ export type DeleteOptions = {| startDate: Date, endDate: Date, |}; +export type AuthParams = { login: string, password: string }; export type LoadParams = {| loadUsers: boolean, |}; @@ -92,6 +103,7 @@ export type DashboardState = {| showPolyline: boolean, startDate: Date, testMarkers: Object, + auth: AuthInfo, |}; // Action Types @@ -198,13 +210,14 @@ type SetOrgTokenFromSearchAction = {| type: 'dashboard/SET_ORG_TOKEN_FROM_SEARCH', value: string, |}; -// Combining Actions type AddTestMarkerAction = {| type: 'dashboard/ADD_TEST_MARKER', value: $Shape<{| data: any |}>, |}; +// Combining Actions + type Action = | AddTestMarkerAction | ApplyExistingSettinsAction @@ -231,187 +244,140 @@ type Action = | SetShowPolylineAction | SetStartDateAction; -type GetState = () => GlobalState; -type Dispatch = (action: Action | ThunkAction) => Promise; // eslint-disable-line no-use-before-define -type ThunkAction = (dispatch: Dispatch, getState: GetState) => Promise; - // ------------------------------------ // Action Creators // ------------------------------------ -export function setOrgTokens (orgTokens: OrgToken[]): SetOrgTokensAction { - return { - type: 'dashboard/SET_ORG_TOKENS', - orgTokens, - }; -} -export function setDevices (devices: Device[]): SetDevicesAction { - return { - type: 'dashboard/SET_DEVICES', - devices, - }; -} +export const setOrgTokens = (orgTokens: OrgToken[]): SetOrgTokensAction => ({ + type: 'dashboard/SET_ORG_TOKENS', + orgTokens, +}); +export const setDevices = (devices: Device[]): SetDevicesAction => ({ + type: 'dashboard/SET_DEVICES', + devices, +}); -export function setLocations (locations: Location[]): SetLocationsAction { - return { - type: 'dashboard/SET_LOCATIONS', - locations, - }; -} +export const setLocations = (locations: Location[]): SetLocationsAction => ({ + type: 'dashboard/SET_LOCATIONS', + locations, +}); -export function setHasData (status: boolean): SetHasDataAction { - return { - type: 'dashboard/SET_HAS_DATA', - status, - }; -} +export const setHasData = (status: boolean): SetHasDataAction => ({ + type: 'dashboard/SET_HAS_DATA', + status, +}); -export function setIsLoading (status: boolean): SetIsLoadingAction { - return { - type: 'dashboard/SET_IS_LOADING', - status, - }; -} +export const setIsLoading = (status: boolean): SetIsLoadingAction => ({ + type: 'dashboard/SET_IS_LOADING', + status, +}); -export function autoselectOrInvalidateSelectedOrgToken (): AutoselectOrInvalidateSelectedOrgTokenAction { - return { type: 'dashboard/AUTOSELECT_OR_INVALIDATE_SELECTED_ORG_TOKEN' }; -} +export const autoselectOrInvalidateSelectedOrgToken = + (): AutoselectOrInvalidateSelectedOrgTokenAction => ( + { type: 'dashboard/AUTOSELECT_OR_INVALIDATE_SELECTED_ORG_TOKEN' } + ); -export function autoselectOrInvalidateSelectedDevice (): AutoselectOrInvalidateSelectedDeviceAction { - return { type: 'dashboard/AUTOSELECT_OR_INVALIDATE_SELECTED_DEVICE' }; -} +export const autoselectOrInvalidateSelectedDevice = + (): AutoselectOrInvalidateSelectedDeviceAction => ({ type: 'dashboard/AUTOSELECT_OR_INVALIDATE_SELECTED_DEVICE' }); -export function invalidateSelectedLocation (): InvalidateSelectedLocationAction { - return { type: 'dashboard/INVALIDATE_SELECTED_LOCATION' }; -} +export const invalidateSelectedLocation = + (): InvalidateSelectedLocationAction => ({ type: 'dashboard/INVALIDATE_SELECTED_LOCATION' }); -export function setShowMarkers (value: boolean): SetShowMarkersAction { - return { - type: 'dashboard/SET_SHOW_MARKERS', - value, - }; -} +export const setShowMarkers = (value: boolean): SetShowMarkersAction => ({ + type: 'dashboard/SET_SHOW_MARKERS', + value, +}); -export function setEnableClustering (value: boolean): SetEnableClusteringAction { - return { - type: 'dashboard/SET_ENABLE_CLUSTERING', - value, - }; -} +export const setEnableClustering = (value: boolean): SetEnableClusteringAction => ({ + type: 'dashboard/SET_ENABLE_CLUSTERING', + value, +}); -export function setShowMaxMarkers (value: number): SetMaxMarkersAction { - return { - type: 'dashboard/SET_MAX_MARKERS', - value, - }; -} +export const setShowMaxMarkers = (value: number): SetMaxMarkersAction => ({ + type: 'dashboard/SET_MAX_MARKERS', + value, +}); -export function setShowPolyline (value: boolean): SetShowPolylineAction { - return { - type: 'dashboard/SET_SHOW_POLYLINE', - value, - }; -} -export function setShowGeofenceHits (value: boolean): SetShowGeofenceHitsAction { - return { - type: 'dashboard/SET_SHOW_GEOFENCE_HITS', - value, - }; -} +export const setShowPolyline = (value: boolean): SetShowPolylineAction => ({ + type: 'dashboard/SET_SHOW_POLYLINE', + value, +}); -export function setIsWatching (value: boolean): SetIsWatchingAction { - return { - type: 'dashboard/SET_IS_WATCHING', - value, - }; -} +export const setShowGeofenceHits = (value: boolean): SetShowGeofenceHitsAction => ({ + type: 'dashboard/SET_SHOW_GEOFENCE_HITS', + value, +}); -export function setCurrentLocation (location: ?Location): SetCurrentLocationAction { - return { - type: 'dashboard/SET_CURRENT_LOCATION', - location, - }; -} +export const setIsWatching = (value: boolean): SetIsWatchingAction => ({ + type: 'dashboard/SET_IS_WATCHING', + value, +}); -export function setStartDate (value: Date): SetStartDateAction { - return { - type: 'dashboard/SET_START_DATE', - value, - }; -} +export const setCurrentLocation = (location: ?Location): SetCurrentLocationAction => ({ + type: 'dashboard/SET_CURRENT_LOCATION', + location, +}); -export function setEndDate (value: Date): SetEndDateAction { - return { - type: 'dashboard/SET_END_DATE', - value, - }; -} +export const setStartDate = (value: Date): SetStartDateAction => ({ + type: 'dashboard/SET_START_DATE', + value, +}); -export function setDevice (deviceId: string): SetDeviceAction { - return { - type: 'dashboard/SET_DEVICE', - deviceId, - }; -} +export const setEndDate = (value: Date): SetEndDateAction => ({ + type: 'dashboard/SET_END_DATE', + value, +}); -export function setSelectedLocation (locationId: string): SetSelectedLocationAction { - return { - type: 'dashboard/SET_SELECTED_LOCATION', - locationId, - }; -} +export const setDevice = (deviceId: string): SetDeviceAction => ({ + type: 'dashboard/SET_DEVICE', + deviceId, +}); -export function unselectLocation (): SetSelectedLocationAction { - return { - type: 'dashboard/SET_SELECTED_LOCATION', - locationId: null, - }; -} +export const setSelectedLocation = (locationId: string): SetSelectedLocationAction => ({ + type: 'dashboard/SET_SELECTED_LOCATION', + locationId, +}); -export function applyExistingSettings (settings: StoredSettings): ApplyExistingSettinsAction { - return { - type: 'dashboard/APPLY_EXISTING_SETTINGS', - settings, - }; -} +export const unselectLocation = (): SetSelectedLocationAction => ({ + type: 'dashboard/SET_SELECTED_LOCATION', + locationId: null, +}); -export function setActiveTab (tab: Tab): SetActiveTabAction { - return { - type: 'dashboard/SET_ACTIVE_TAB', - tab, - }; -} +export const applyExistingSettings = (settings: StoredSettings): ApplyExistingSettinsAction => ({ + type: 'dashboard/APPLY_EXISTING_SETTINGS', + settings, +}); -export function setOrgToken (value: string): SetOrgTokenAction { - return { - type: 'dashboard/SET_ORG_TOKEN', - value, - }; -} +export const setActiveTab = (tab: Tab): SetActiveTabAction => ({ + type: 'dashboard/SET_ACTIVE_TAB', + tab, +}); -export function setOrgTokenFromSearch (value: string): SetOrgTokenFromSearchAction { - return { - type: 'dashboard/SET_ORG_TOKEN_FROM_SEARCH', - value, - }; -} +export const setOrgToken = (value: string): SetOrgTokenAction => ({ + type: 'dashboard/SET_ORG_TOKEN', + value, +}); -export function doAddTestMarker (value: Object): AddTestMarkerAction { - return { - type: 'dashboard/ADD_TEST_MARKER', - value, - }; -} +export const setOrgTokenFromSearch = (value: string): SetOrgTokenFromSearchAction => ({ + type: 'dashboard/SET_ORG_TOKEN_FROM_SEARCH', + value, +}); + +export const doAddTestMarker = (value: Object): AddTestMarkerAction => ({ + type: 'dashboard/ADD_TEST_MARKER', + value, +}); // ------------------------------------ // Thunk Actions // ------------------------------------ export const loadOrgTokens = (): ThunkAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const { dashboard: { orgTokenFromSearch } } = getState(); + const { dashboard: { orgTokenFromSearch }, auth } = getState(); const params = qs.stringify({ company_token: orgTokenFromSearch }); try { - const response = await fetch(`${API_URL}/company_tokens?${params}`); + const headers = makeHeaders(auth); + const response = await fetch(`${API_URL}/company_tokens?${params}`, { headers }); const records = await response.json(); const orgTokens: OrgToken[] = records.map((x: { company_token: string }) => ({ id: x.id, @@ -425,13 +391,14 @@ export const loadOrgTokens = (): ThunkAction => async (dispatch: Dispatch, getSt }; export const loadDevices = (): ThunkAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const { dashboard: { companyId, orgToken } } = getState(); + const { dashboard: { companyId, orgToken }, auth } = getState(); const params = qs.stringify({ company_id: companyId, company_token: orgToken, }); try { - const response = await fetch(`${API_URL}/devices?${params}`); + const headers = makeHeaders(auth); + const response = await fetch(`${API_URL}/devices?${params}`, { headers }); const records = await response.json(); const devices: Device[] = records .map((record: Object) => ({ @@ -448,8 +415,11 @@ export const loadDevices = (): ThunkAction => async (dispatch: Dispatch, getStat export const loadLocations = (): ThunkAction => async (dispatch: Dispatch, getState: GetState): Promise => { const { - deviceId, orgToken, companyId, startDate, endDate, maxMarkers, - } = getState().dashboard; + dashboard: { + deviceId, orgToken, companyId, startDate, endDate, maxMarkers, + }, + auth, + } = getState(); GA.sendEvent('tracker', 'loadLocations', orgToken); const params = qs.stringify({ @@ -461,7 +431,8 @@ export const loadLocations = (): ThunkAction => async (dispatch: Dispatch, getSt start_date: startDate.toISOString(), }); try { - const response = await fetch(`${API_URL}/locations?${params}`); + const headers = makeHeaders(auth); + const response = await fetch(`${API_URL}/locations?${params}`, { headers }); const records = await response.json(); return dispatch(setLocations(records)); } catch (e) { @@ -472,8 +443,11 @@ export const loadLocations = (): ThunkAction => async (dispatch: Dispatch, getSt export const loadCurrentLocation = (): ThunkAction => async (dispatch: Dispatch, getState: GetState): Promise => { const { - deviceId, companyId, company_token: orgToken, - } = getState().dashboard; + dashboard: { + deviceId, companyId, company_token: orgToken, + }, + auth, + } = getState(); if (deviceId) { const params = qs.stringify({ device_id: deviceId, @@ -481,7 +455,8 @@ export const loadCurrentLocation = (): ThunkAction => async (dispatch: Dispatch, company_token: orgToken, }); try { - const response = await fetch(`${API_URL}/locations/latest?${params}`); + const headers = makeHeaders(auth); + const response = await fetch(`${API_URL}/locations/latest?${params}`, { headers }); const currentLocation = await response.json(); return await dispatch(setCurrentLocation(currentLocation)); } catch (e) { @@ -527,9 +502,9 @@ export const loadInitialData = GA.sendEvent('tracker', `load:${id}`); }; -export function deleteActiveDevice (deleteOptions: ?DeleteOptions): ThunkAction { - return async (dispatch: Dispatch, getState: GetState): Promise => { - const { dashboard: { deviceId } } = getState(); +export const deleteActiveDevice = + (deleteOptions: ?DeleteOptions): ThunkAction => async (dispatch: Dispatch, getState: GetState): Promise => { + const { dashboard: { deviceId }, auth } = getState(); if (!deviceId) { return; } @@ -539,43 +514,43 @@ export function deleteActiveDevice (deleteOptions: ?DeleteOptions): ThunkAction end_date: deleteOptions.endDate.toISOString(), })}` : ''; + const headers = makeHeaders(auth); try { - await fetch(`${API_URL}/devices/${deviceId}${params}`, { method: 'delete' }); + await fetch(`${API_URL}/devices/${deviceId}${params}`, { method: 'delete', headers }); await dispatch(reload({ loadUsers: false })); GA.sendEvent('tracker', `delete device:${deviceId}`); } catch (e) { console.error('deleteActiveDevice', e); } }; -} export function changeStartDate (value: Date) { return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setStartDate(value)); - setSettings(getState().dashboard.orgTokenFromSearch, { startDate: value }); - const { dashboard } = getState(); + const { dashboard, auth } = getState(); + setSettings(dashboard.orgTokenFromSearch, { startDate: value }); setUrlSettings({ startDate: dashboard.startDate, endDate: dashboard.endDate, deviceId: dashboard.deviceId, companyId: dashboard.companyId, orgTokenFromSearch: dashboard.orgTokenFromSearch, - }); + }, auth); await dispatch(reload({ loadUsers: false })); }; } export function changeEndDate (value: Date) { return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setEndDate(value)); - setSettings(getState().dashboard.orgTokenFromSearch, { endDate: value }); - const { dashboard } = getState(); + const { dashboard, auth } = getState(); + setSettings(dashboard.orgTokenFromSearch, { endDate: value }); setUrlSettings({ startDate: dashboard.startDate, endDate: dashboard.endDate, deviceId: dashboard.deviceId, companyId: dashboard.companyId, orgTokenFromSearch: dashboard.orgTokenFromSearch, - }); + }, auth); await dispatch(reload({ loadUsers: false })); }; } @@ -590,15 +565,15 @@ export function changeOrgToken (value: string) { export function changeDeviceId (value: string) { return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setDevice(value)); - setSettings(getState().dashboard.orgTokenFromSearch, { deviceId: value }); - const { dashboard } = getState(); + const { dashboard, auth } = getState(); + setSettings(dashboard.orgTokenFromSearch, { deviceId: value }); setUrlSettings({ startDate: dashboard.startDate, endDate: dashboard.endDate, deviceId: dashboard.deviceId, companyId: dashboard.companyId, orgTokenFromSearch: dashboard.orgTokenFromSearch, - }); + }, auth); await dispatch(reload({ loadUsers: false })); }; } @@ -606,42 +581,48 @@ export function changeDeviceId (value: string) { export function changeIsWatching (value: boolean) { return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setIsWatching(value)); - setSettings(getState().dashboard.orgTokenFromSearch, { isWatching: value }); + const { dashboard } = getState(); + setSettings(dashboard.orgTokenFromSearch, { isWatching: value }); }; } export function changeShowMarkers (value: boolean) { return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setShowMarkers(value)); - setSettings(getState().dashboard.orgTokenFromSearch, { showMarkers: value }); + const { dashboard } = getState(); + setSettings(dashboard.orgTokenFromSearch, { showMarkers: value }); }; } export function changeEnableClustering (value: boolean) { return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setEnableClustering(value)); - setSettings(getState().dashboard.orgTokenFromSearch, { enableClustering: value }); + const { dashboard } = getState(); + setSettings(dashboard.orgTokenFromSearch, { enableClustering: value }); }; } export function changeShowPolyline (value: boolean) { return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setShowPolyline(value)); - setSettings(getState().dashboard.orgTokenFromSearch, { showPolyline: value }); + const { dashboard } = getState(); + setSettings(dashboard.orgTokenFromSearch, { showPolyline: value }); }; } export function changeShowGeofenceHits (value: boolean) { return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setShowGeofenceHits(value)); - setSettings(getState().dashboard.orgTokenFromSearch, { showGeofenceHits: value }); + const { dashboard } = getState(); + setSettings(dashboard.orgTokenFromSearch, { showGeofenceHits: value }); }; } export function changeMaxMarkers (value: number) { return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setShowMaxMarkers(value)); - setSettings(getState().dashboard.orgTokenFromSearch, { maxMarkers: value }); + const { dashboard } = getState(); + setSettings(dashboard.orgTokenFromSearch, { maxMarkers: value }); }; } @@ -660,11 +641,8 @@ export function changeActiveTab (tab: Tab) { }; } -export function addTestMarker (value: Object): ThunkAction { - return async (dispatch: Dispatch): Promise => { - dispatch(doAddTestMarker(value)); - }; -} +export const addTestMarker = + (value: Object): ThunkAction => async (dispatch: Dispatch): Promise => dispatch(doAddTestMarker(value)); // ------------------------------------ // Action Handlers // ------------------------------------ diff --git a/src/client/reducer/index.js b/src/client/reducer/index.js index 3ecba30..8a82900 100644 --- a/src/client/reducer/index.js +++ b/src/client/reducer/index.js @@ -1,5 +1,6 @@ import { combineReducers } from 'redux'; import dashboard from './dashboard'; +import auth from './auth'; -export default combineReducers({ dashboard }); +export default combineReducers({ dashboard, auth }); diff --git a/src/client/reducer/state.js b/src/client/reducer/state.js index f333d70..e9d35a8 100644 --- a/src/client/reducer/state.js +++ b/src/client/reducer/state.js @@ -1,8 +1,10 @@ // @flow -import { type DashboardState } from './index'; +import { type AuthState } from './auth'; +import { type DashboardState } from './dashboard'; export type GlobalState = { dashboard: DashboardState, + auth: AuthState, }; export type Tab = 'map' | 'list'; diff --git a/src/client/reducer/types.js b/src/client/reducer/types.js new file mode 100644 index 0000000..790dcca --- /dev/null +++ b/src/client/reducer/types.js @@ -0,0 +1,6 @@ +// @flow +import { type GlobalState } from 'reducer/state'; + +export type GetState = () => GlobalState; +export type Dispatch = (action: Action | ThunkAction) => Promise; // eslint-disable-line no-use-before-define +export type ThunkAction = (dispatch: Dispatch, getState: GetState) => Promise; diff --git a/src/client/storage.js b/src/client/storage.js index ae326dc..639990f 100644 --- a/src/client/storage.js +++ b/src/client/storage.js @@ -4,6 +4,7 @@ import isUndefined from 'lodash/isUndefined'; import omitBy from 'lodash/omitBy'; import { type Tab } from 'reducer/state'; +import { type AuthInfo } from 'reducer/types'; import cloneState from 'utils/cloneState'; export type StoredSettings = {| @@ -18,8 +19,28 @@ export type StoredSettings = {| showMarkers: boolean, maxMarkers: number, |}; + + const getLocalStorageKey = (key: string) => (key ? `settings#${key}` : 'settings'); +export function getAuth(): AuthSettings { + const encodedSettings = localStorage.getItem(getLocalStorageKey('auth')); + if (encodedSettings) { + const parsed = JSON.parse(encodedSettings); + return parsed; + } + return null; +} + +export function setAuth(settings: AuthSettings): AuthSettings { + if (!settings) { + return null; + } + localStorage.setItem(getLocalStorageKey('auth'), JSON.strinfigy(settings)); + + return settings; +} + export function getSettings(key: string): $Shape { const encodedSettings = localStorage.getItem(getLocalStorageKey(key)); if (encodedSettings) { @@ -103,22 +124,27 @@ export function getUrlSettings(): $Shape { ); return result; } -export function setUrlSettings(settings: {| - deviceId: ?string, - startDate: ?Date, - endDate: ?Date, - orgTokenFromSearch: string, -|}) { +export function setUrlSettings( + settings: {| + deviceId: ?string, + startDate: ?Date, + endDate: ?Date, + orgTokenFromSearch: string, + |}, + auth: AuthInfo, +) { const { orgTokenFromSearch, startDate, endDate, deviceId, } = settings; + const { accessToken } = auth; + const hasToken = accessToken || !!process.env.SHARED_DASHBOARD; const mainPart = orgTokenFromSearch ? `/${orgTokenFromSearch}` : ''; const search = { device: deviceId, end: encodeEndDate(endDate), start: encodeStartDate(startDate), }; - const url = `${mainPart}?${queryString.stringify(search)}`; + const url = `${!hasToken ? mainPart : ''}?${queryString.stringify(search)}`; window.history.replaceState({}, '', url); } diff --git a/src/client/utils/request.js b/src/client/utils/request.js new file mode 100644 index 0000000..4fa8574 --- /dev/null +++ b/src/client/utils/request.js @@ -0,0 +1,13 @@ +// @flow + +// eslint-disable-next-line import/prefer-default-export +export const makeHeaders = (authInfo: AuthInfo): Object => { + const { accessToken } = authInfo; + const headers = { 'Content-Type': 'application/json' }; + + if (accessToken) { + headers.Authorization = `Bearer ${accessToken}`; + } + + return headers; +}; diff --git a/src/server/libs/utils.js b/src/server/libs/utils.js index 03b5513..48cd94b 100644 --- a/src/server/libs/utils.js +++ b/src/server/libs/utils.js @@ -12,7 +12,7 @@ import { verify } from './jwt'; // and it will hit /v2/registration once again. const DUMMY_TOKEN = 'DUMMY_TOKEN'; -export const filterByCompany = !!process.env.SHARED_DASHBOARD; +export const withoutAuth = !!process.env.SHARED_DASHBOARD; export const deniedCompanies = (process.env.DENIED_COMPANY_TOKENS || '').split(','); export const deniedDevices = (process.env.DENIED_DEVICE_TOKENS || '').split(','); export const ddosBombCompanies = ( @@ -26,8 +26,9 @@ const check = (list, item) => list.find(x => !!x && (item || '').toLowerCase().s export const isDDosCompany = orgToken => check(ddosBombCompanies, orgToken); export const isDeniedCompany = orgToken => check(deniedCompanies, orgToken); export const isDeniedDevice = orgToken => check(deniedDevices, orgToken); -export const isAdmin = orgToken => !!filterByCompany & !!process.env.ADMIN_TOKEN && +export const isAdminToken = orgToken => !!process.env.ADMIN_TOKEN && orgToken === process.env.ADMIN_TOKEN; +export const isPassword = password => process.env.PASSWORD === password; export const jsonb = data => (isPostgres ? data || null : JSON.stringify(data)); @@ -108,6 +109,23 @@ export const checkAuth = (req, res, next) => { } }; +export const getAuth = (req, res, next) => { + const auth = (req.get('Authorization') || '').split(' '); + + if (auth.length >= 2 && auth[0] === 'Bearer') { + const [, jwt] = auth; + try { + const decoded = verify(jwt); + req.jwt = decoded; + return next(); + } catch (e) { + // eslint-disable-next-line no-console + console.warn('getAuth', e); + } + } + return next(); +}; + export const checkCompany = ({ org, model }) => { if (isDeniedCompany(org)) { throw new AccessDeniedError( diff --git a/src/server/models/Device.js b/src/server/models/Device.js index 77f3d87..3960c93 100644 --- a/src/server/models/Device.js +++ b/src/server/models/Device.js @@ -3,9 +3,7 @@ import { Op } from 'sequelize'; import DeviceModel from '../database/DeviceModel'; import LocationModel from '../database/LocationModel'; -import { - checkCompany, filterByCompany, desc, -} from '../libs/utils'; +import { checkCompany, desc } from '../libs/utils'; import { findOrCreate as findOrCreateCompany } from './Org'; @@ -25,11 +23,13 @@ export async function getDevice({ id }) { return result; } -export async function getDevices(params) { - const whereConditions = {}; - if (filterByCompany) { - params.company_id && (whereConditions.company_id = +params.company_id); +export async function getDevices(params, isAdmin) { + if (!isAdmin && !params.company_id) { + return []; } + + const whereConditions = {}; + params.company_id && (whereConditions.company_id = +params.company_id); const result = await DeviceModel.findAll({ where: whereConditions, attributes: [ diff --git a/src/server/models/Location.js b/src/server/models/Location.js index e76b8cf..261a1d0 100644 --- a/src/server/models/Location.js +++ b/src/server/models/Location.js @@ -7,7 +7,6 @@ import DeviceModel from '../database/DeviceModel'; import LocationModel from '../database/LocationModel'; import { AccessDeniedError, - filterByCompany, desc, hydrate, isDeniedCompany, @@ -31,16 +30,18 @@ export async function getStats() { }; } -export async function getLocations(params) { +export async function getLocations(params, isAdmin) { + if (!isAdmin && !(params.device_id || params.company_id)) { + return []; + } + const whereConditions = {}; if (params.start_date && params.end_date) { whereConditions.recorded_at = { [Op.between]: [new Date(params.start_date), new Date(params.end_date)] }; } params.device_id && (whereConditions.device_id = +params.device_id); - if (filterByCompany) { - params.company_id && (whereConditions.company_id = +params.company_id); - } + params.company_id && (whereConditions.company_id = +params.company_id); const rows = await LocationModel.findAll({ where: whereConditions, @@ -53,13 +54,17 @@ export async function getLocations(params) { return locations; } -export async function getLatestLocation(params) { +export async function getLatestLocation(params, isAdmin) { + if (!isAdmin && !(params.device_id || params.company_id || params.companyId)) { + return []; + } + const whereConditions = {}; + params.device_id && (whereConditions.device_id = +params.device_id); - if (filterByCompany) { - params.companyId && (whereConditions.company_id = +params.companyId); - params.company_id && (whereConditions.company_id = +params.company_id); - } + params.companyId && (whereConditions.company_id = +params.companyId); + params.company_id && (whereConditions.company_id = +params.company_id); + const row = await LocationModel.findOne({ where: whereConditions, order: [['recorded_at', desc]], @@ -152,13 +157,17 @@ export async function createLocation(params, device = {}) { ); } -export async function deleteLocations(params) { +export async function deleteLocations(params, isAdmin) { + if (!isAdmin && !(params.companyId || params.deviceId)) { + return; + } + const whereConditions = {}; const verify = {}; const companyId = params && (params.companyId || params.company_id); const deviceId = params && (params.deviceId || params.device_id); - if (filterByCompany && !!companyId) { + if (companyId) { whereConditions.company_id = +companyId; verify.company_id = +companyId; } diff --git a/src/server/models/Org.js b/src/server/models/Org.js index 7c2d137..5eaf02b 100644 --- a/src/server/models/Org.js +++ b/src/server/models/Org.js @@ -1,11 +1,9 @@ -import { - isAdmin, filterByCompany, desc, -} from '../libs/utils'; +import { desc } from '../libs/utils'; import CompanyModel from '../database/CompanyModel'; -export async function getOrgs({ company_token: org }) { - if (!filterByCompany) { +export async function getOrgs({ company_token: org }, isAdmin) { + if (!isAdmin && !org) { return [ { id: 1, @@ -13,7 +11,8 @@ export async function getOrgs({ company_token: org }) { }, ]; } - const whereConditions = isAdmin(org) ? {} : { company_token: org }; + + const whereConditions = isAdmin ? {} : { company_token: org }; const result = await CompanyModel.findAll({ where: whereConditions, attributes: ['id', 'company_token'], diff --git a/src/server/routes/site-api.js b/src/server/routes/site-api.js index ebb9ae9..9bf1549 100644 --- a/src/server/routes/site-api.js +++ b/src/server/routes/site-api.js @@ -6,9 +6,12 @@ import { sign } from '../libs/jwt'; import { decrypt, isEncryptedRequest } from '../libs/RNCrypto'; import { AccessDeniedError, - isAdmin, + isAdminToken, isDDosCompany, + isPassword, return1Gbfile, + getAuth, + withoutAuth, } from '../libs/utils'; import { deleteDevice, getDevices } from '../models/Device'; import { @@ -25,9 +28,9 @@ const router = new Router(); /** * GET /company_tokens */ -router.get('/company_tokens', async (req, res) => { +router.get('/company_tokens', getAuth, async (req, res) => { try { - const orgs = await getOrgs(req.query); + const orgs = await getOrgs(req.query, !!req.jwt || withoutAuth); res.send(orgs); } catch (err) { console.error('/company_tokens', err); @@ -38,9 +41,9 @@ router.get('/company_tokens', async (req, res) => { /** * GET /devices */ -router.get('/devices', async (req, res) => { +router.get('/devices', getAuth, async (req, res) => { try { - const devices = await getDevices(req.query); + const devices = await getDevices(req.query, !!req.jwt || withoutAuth); res.send(devices); } catch (err) { console.error('/devices', err); @@ -48,7 +51,7 @@ router.get('/devices', async (req, res) => { } }); -router.delete('/devices/:id', async (req, res) => { +router.delete('/devices/:id', getAuth, async (req, res) => { try { console.log( `DELETE /devices/${req.params.id}?${JSON.stringify(req.query)}\n`.green, @@ -66,7 +69,7 @@ router.delete('/devices/:id', async (req, res) => { } }); -router.get('/stats', async (req, res) => { +router.get('/stats', getAuth, async (req, res) => { try { const stats = await getStats(); res.send(stats); @@ -76,10 +79,10 @@ router.get('/stats', async (req, res) => { } }); -router.get('/locations/latest', async (req, res) => { +router.get('/locations/latest', getAuth, async (req, res) => { console.log('GET /locations %s'.green, JSON.stringify(req.query)); try { - const latest = await getLatestLocation(req.query); + const latest = await getLatestLocation(req.query, !!req.jwt || withoutAuth); res.send(latest); } catch (err) { console.info('/locations/latest', JSON.stringify(req.query), err); @@ -90,11 +93,11 @@ router.get('/locations/latest', async (req, res) => { /** * GET /locations */ -router.get('/locations', async (req, res) => { +router.get('/locations', getAuth, async (req, res) => { console.log('GET /locations %s'.green, JSON.stringify(req.query)); try { - const locations = await getLocations(req.query); + const locations = await getLocations(req.query, !!req.jwt || withoutAuth); res.send(locations); } catch (err) { console.info('get /locations', JSON.stringify(req.query), err); @@ -105,7 +108,7 @@ router.get('/locations', async (req, res) => { /** * POST /locations */ -router.post('/locations', async (req, res) => { +router.post('/locations', getAuth, async (req, res) => { const { body } = req; const data = isEncryptedRequest(req) ? decrypt(body.toString()) : body; const locations = Array.isArray(data) ? data : data ? [data] : []; @@ -129,7 +132,7 @@ router.post('/locations', async (req, res) => { /** * POST /locations */ -router.post('/locations/:company_token', async (req, res) => { +router.post('/locations/:company_token', getAuth, async (req, res) => { const { company_token: org } = req.params; console.info('locations:post'.green, 'org:name'.green, org); @@ -156,11 +159,11 @@ router.post('/locations/:company_token', async (req, res) => { } }); -router.delete('/locations', async (req, res) => { +router.delete('/locations', getAuth, async (req, res) => { console.info('locations:delete:query'.green, JSON.stringify(req.query)); try { - await deleteLocations(req.query); + await deleteLocations(req.query, !!req.jwt || withoutAuth); res.send({ success: true }); res.status(500).send({ error: 'Something failed!' }); @@ -189,16 +192,24 @@ router.post('/configure', async (req, res) => { }); router.post('/auth', async (req, res) => { - const { body: { org } } = req; + const { login, password } = req.body || {}; - if (isAdmin(org)) { - const jwtInfo = { org }; - - const accessToken = sign(jwtInfo); - res.send({ accessToken }); + try { + if (isAdminToken(login) && isPassword(password)) { + const jwtInfo = { org: login }; + + const accessToken = sign(jwtInfo); + return res.send({ + access_token: accessToken, + token_type: 'Bearer', + org: login, + }); + } + } catch (e) { + console.error('/auth', e); } - return res.status(401).send({ org, error: 'Await not public account' }); + return res.status(401).send({ org: login, error: 'Await not public account and right password' }); }); /** diff --git a/webpack.config.js b/webpack.config.js index 77088e1..c4d620c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -124,7 +124,7 @@ const config = { plugins: isProduction ? [ new webpack.DefinePlugin({ - 'process.env.SHARED_DASHBOARD': !!process.env.SHARED_DASHBOARD || '', + 'process.env.SHARED_DASHBOARD': !!process.env.SHARED_DASHBOARD || '""', 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), }), new webpack.LoaderOptionsPlugin({ @@ -137,7 +137,7 @@ const config = { : [ new webpack.NamedModulesPlugin(), new webpack.DefinePlugin({ - 'process.env.SHARED_DASHBOARD': !!process.env.SHARED_DASHBOARD || '', + 'process.env.SHARED_DASHBOARD': !!process.env.SHARED_DASHBOARD || '""', 'process.env.NODE_ENV': JSON.stringify( process.env.NODE_ENV || 'development', ),