try {
expect(auth).toEqual({
apiToken: "token",
+ config: {
+ remoteHosts: {
+ "xc59z": "xc59z.arvadosapi.com",
+ },
+ rootUrl: "https://zzzzz.arvadosapi.com",
+ uuidPrefix: "zzzzz",
+ },
sshKeys: [],
homeCluster: "zzzzz",
localCluster: "zzzzz",
export const authActions = unionize({
LOGIN: {},
LOGOUT: ofType<{ deleteLinkData: boolean }>(),
- CONFIG: ofType<{ config: Config }>(),
- INIT: ofType<{ user: User, token: string }>(),
+ SET_CONFIG: ofType<{ config: Config }>(),
+ INIT_USER: ofType<{ user: User, token: string }>(),
USER_DETAILS_REQUEST: {},
USER_DETAILS_SUCCESS: ofType<User>(),
SET_SSH_KEYS: ofType<SshKeyResource[]>(),
if (homeCluster && !config.remoteHosts[homeCluster]) {
homeCluster = undefined;
}
- dispatch(authActions.CONFIG({ config }));
+ dispatch(authActions.SET_CONFIG({ config }));
dispatch(authActions.SET_HOME_CLUSTER(config.loginCluster || homeCluster || config.uuidPrefix));
if (token && token !== "undefined") {
const svc = createServices(config, { progressFn: () => { }, errorFn: () => { } });
setAuthorizationHeader(svc, token);
return svc.authService.getUserDetails().then((user: User) => {
- dispatch(authActions.INIT({ user, token }));
+ dispatch(authActions.INIT_USER({ user, token }));
});
};
import { WORKBENCH_LOADING_SCREEN } from '~/store/workbench/workbench-actions';
export const authMiddleware = (services: ServiceRepository): Middleware => store => next => action => {
+ // Middleware to update external state (local storage, window
+ // title) to ensure that they stay in sync with redux state.
+
authActions.match(action, {
- INIT: ({ user, token }) => {
+ INIT_USER: ({ user, token }) => {
+ // The "next" method passes the action to the next
+ // middleware in the chain, or the reducer. That means
+ // after next() returns, the action has (presumably) been
+ // applied by the reducer to update the state.
next(action);
+
const state: RootState = store.getState();
if (state.auth.apiToken) {
store.dispatch<any>(initSessions(services.authService, state.auth.remoteHostsConfig[state.auth.localCluster], user));
if (!user.isActive) {
+ // As a special case, if the user is inactive, they
+ // may be able to self-activate using the "activate"
+ // method. Note, for this to work there can't be any
+ // unsigned user agreements, we assume the API server is just going to
+ // rubber-stamp our activation request. At some point in the future we'll
+ // want to either add support for displaying/signing user
+ // agreements or get rid of self-activation.
+ // For more details, see:
+ // https://doc.arvados.org/master/admin/user-management.html
+
store.dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
services.userService.activate(user.uuid).then((user: User) => {
- store.dispatch(authActions.INIT({ user, token }));
+ store.dispatch(authActions.INIT_USER({ user, token }));
store.dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
}).catch(() => {
store.dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
});
}
},
- CONFIG: ({ config }) => {
+ SET_CONFIG: ({ config }) => {
document.title = `Arvados Workbench (${config.uuidPrefix})`;
next(action);
},
isAdmin: false,
isActive: true
};
- const state = reducer(initialState, authActions.INIT({ user, token: "token" }));
+ const state = reducer(initialState, authActions.INIT_USER({ user, token: "token" }));
expect(state).toEqual({
apiToken: "token",
+ config: mockConfig({}),
user,
sshKeys: [],
sessions: [],
const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
expect(state).toEqual({
apiToken: undefined,
+ config: mockConfig({}),
sshKeys: [],
sessions: [],
homeCluster: "uuid",
import { ServiceRepository } from "~/services/services";
import { SshKeyResource } from '~/models/ssh-key';
import { Session } from "~/models/session";
-import { Config } from '~/common/config';
+import { Config, mockConfig } from '~/common/config';
export interface AuthState {
user?: User;
loginCluster: string;
remoteHosts: { [key: string]: string };
remoteHostsConfig: { [key: string]: Config };
+ config: Config;
}
const initialState: AuthState = {
homeCluster: "",
loginCluster: "",
remoteHosts: {},
- remoteHostsConfig: {}
+ remoteHostsConfig: {},
+ config: mockConfig({})
};
export const authReducer = (services: ServiceRepository) => (state = initialState, action: AuthAction) => {
return authActions.match(action, {
- CONFIG: ({ config }) => {
+ SET_CONFIG: ({ config }) => {
return {
...state,
+ config,
localCluster: config.uuidPrefix,
remoteHosts: { ...config.remoteHosts, [config.uuidPrefix]: new URL(config.rootUrl).host },
homeCluster: config.loginCluster || config.uuidPrefix,
remoteHostsConfig: { ...state.remoteHostsConfig, [config.uuidPrefix]: config },
};
},
- INIT: ({ user, token }) => {
+ INIT_USER: ({ user, token }) => {
return { ...state, user, apiToken: token, homeCluster: user.uuid.substr(0, 5) };
},
LOGIN: () => {
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { ofType, unionize, UnionOf } from '~/common/unionize';
-import { Config } from '~/common/config';
-
-export const configActions = unionize({
- CONFIG: ofType<{ config: Config }>(),
-});
-
-export type ConfigAction = UnionOf<typeof configActions>;
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { configActions, ConfigAction } from "./config-action";
-import { mockConfig } from '~/common/config';
-
-export const configReducer = (state = mockConfig({}), action: ConfigAction) => {
- return configActions.match(action, {
- CONFIG: ({ config }) => {
- return {
- ...state, ...config
- };
- },
- default: () => state
- });
-};
export const switchUser = (user: UserResource, token: string) =>
(dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- dispatch(authActions.INIT({ user, token }));
+ dispatch(authActions.INIT_USER({ user, token }));
};
export const linkFailed = () =>
}
try {
api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
- const uuidPrefix = api.getState().config.uuidPrefix;
+ const uuidPrefix = api.getState().auth.config.uuidPrefix;
const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
const responseLinks = await this.services.linkService.list({
limit: dataExplorer.rowsPerPage,
snackbarActions.OPEN_SNACKBAR({
message: 'Could not fetch public favorites contents.',
kind: SnackbarKind.ERROR
- });
\ No newline at end of file
+ });
export const togglePublicFavorite = (resource: { uuid: string; name: string }) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
dispatch(progressIndicatorActions.START_WORKING("togglePublicFavorite"));
- const uuidPrefix = getState().config.uuidPrefix;
+ const uuidPrefix = getState().auth.config.uuidPrefix;
const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
dispatch(publicFavoritesActions.TOGGLE_PUBLIC_FAVORITE({ resourceUuid: resource.uuid }));
const isPublicFavorite = checkPublicFavorite(resource.uuid, getState().publicFavorites);
export const updatePublicFavorites = (resourceUuids: string[]) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const uuidPrefix = getState().config.uuidPrefix;
+ const uuidPrefix = getState().auth.config.uuidPrefix;
const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
dispatch(publicFavoritesActions.CHECK_PRESENCE_IN_PUBLIC_FAVORITES(resourceUuids));
services.favoriteService
import { authReducer } from "./auth/auth-reducer";
import { authMiddleware } from "./auth/auth-middleware";
-import { configReducer } from "./config/config-reducer";
import { dataExplorerReducer } from './data-explorer/data-explorer-reducer';
import { detailsPanelReducer } from './details-panel/details-panel-reducer';
import { contextMenuReducer } from './context-menu/context-menu-reducer';
const createRootReducer = (services: ServiceRepository) => combineReducers({
auth: authReducer(services),
- config: configReducer,
collectionPanel: collectionPanelReducer,
collectionPanelFiles: collectionPanelFilesReducer,
contextMenu: contextMenuReducer,
export const loadPublicFavoritesProject = (params: LoadFavoritesProjectParams) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const { pickerId, includeCollections = false, includeFiles = false } = params;
- const uuidPrefix = getState().config.uuidPrefix;
+ const uuidPrefix = getState().auth.config.uuidPrefix;
const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
if (uuid) {
const data = getResource<UserResource>(uuid)(resources);
const client = await services.apiClientAuthorizationService.create({ ownerUuid: uuid });
if (data) {
- dispatch<any>(authActions.INIT({ user: data, token: `v2/${client.uuid}/${client.apiToken}` }));
+ dispatch<any>(authActions.INIT_USER({ user: data, token: `v2/${client.uuid}/${client.apiToken}` }));
location.reload();
dispatch<any>(navigateToRootProject);
}
const mapStateToProps = (state: RootState): AccountMenuProps => ({
user: state.auth.user,
currentRoute: state.router.location ? state.router.location.pathname : '',
- workbenchURL: state.config.workbenchUrl,
+ workbenchURL: state.auth.config.workbenchUrl,
apiToken: state.auth.apiToken,
localCluster: state.auth.localCluster
});
type InactivePanelProps = WithStyles<CssRules> & InactivePanelActionProps & InactivePanelStateProps;
export const InactivePanel = connect((state: RootState) => ({
- inactivePageText: state.config.clusterConfig.Workbench.InactivePageHTML
+ inactivePageText: state.auth.config.clusterConfig.Workbench.InactivePageHTML
}), mapDispatchToProps)(withStyles(styles)((({ classes, startLinking, inactivePageText }: InactivePanelProps) =>
<Grid container justify="center" alignItems="center" direction="column" spacing={24}
className={classes.root}
homeCluster: state.auth.homeCluster,
uuidPrefix: state.auth.localCluster,
loginCluster: state.auth.loginCluster,
- welcomePage: state.config.clusterConfig.Workbench.WelcomePageHTML
+ welcomePage: state.auth.config.clusterConfig.Workbench.WelcomePageHTML
}))(({ classes, dispatch, remoteHosts, homeCluster, uuidPrefix, loginCluster, welcomePage }: LoginPanelProps) =>
<Grid container justify="center" alignItems="center"
className={classes.root}
uuidPrefix: state.auth.localCluster,
isNotLinking: state.linkAccountPanel.status === LinkAccountPanelStatus.NONE || state.linkAccountPanel.status === LinkAccountPanelStatus.INITIAL,
isLinkingPath: state.router.location ? matchLinkAccountRoute(state.router.location.pathname) !== null : false,
- siteBanner: state.config.clusterConfig.Workbench.SiteName
+ siteBanner: state.auth.config.clusterConfig.Workbench.SiteName
};
};