},
errorFn: (id, error) => {
console.error("Backend error:", error);
- store.dispatch(snackbarActions.OPEN_SNACKBAR({
- message: `${error.errors[0]}`,
- kind: SnackbarKind.ERROR,
- hideDuration: 8000
- }));
+ if (error.errors) {
+ store.dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: `${error.errors[0]}`,
+ kind: SnackbarKind.ERROR,
+ hideDuration: 8000
+ }));
+ } else {
+ store.dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: `${error.message}`,
+ kind: SnackbarKind.ERROR,
+ hideDuration: 8000
+ }));
+ }
}
});
const store = configureStore(history, services);
return user ? `${user.firstName} ${user.lastName}` : "";
};
-export interface UserResource extends Resource {
+export interface UserResource extends Resource, User {
kind: ResourceKind.USER;
- email: string;
- username: string;
- firstName: string;
- lastName: string;
- isAdmin: boolean;
- prefs: UserPrefs;
defaultOwnerUuid: string;
- isActive: boolean;
writableBy: string[];
}
throw e;
});
}
-
+
public getRootUuid() {
const uuid = this.getOwnerUuid();
const uuidParts = uuid ? uuid.split('-') : [];
export type ServiceRepository = ReturnType<typeof createServices>;
+export function setAuthorizationHeader(services: ServiceRepository, token: string) {
+ services.apiClient.defaults.headers.common = {
+ Authorization: `Bearer ${token}`
+ };
+ services.webdavClient.defaults.headers = {
+ Authorization: `Bearer ${token}`
+ };
+}
+
+export function removeAuthorizationHeader(services: ServiceRepository) {
+ delete services.apiClient.defaults.headers.common;
+ delete services.webdavClient.defaults.headers.common;
+}
+
export const createServices = (config: Config, actions: ApiActions) => {
- const apiClient = Axios.create();
+ // Need to give empty 'headers' object or it will create an
+ // instance with a reference to the global default headers object,
+ // which is very bad because that means setAuthorizationHeader
+ // would update the global default instead of the instance default.
+ const apiClient = Axios.create({ headers: {} });
apiClient.defaults.baseURL = config.baseUrl;
const webdavClient = new WebDAV();
import { Dispatch } from "redux";
import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
import { RootState } from "~/store/store";
-import { ServiceRepository } from "~/services/services";
+import { ServiceRepository, createServices, setAuthorizationHeader } from "~/services/services";
import Axios from "axios";
import { getUserFullname, User } from "~/models/user";
import { authActions } from "~/store/auth/auth-action";
import { normalizeURLPath } from "~/common/url";
import { Session, SessionStatus } from "~/models/session";
import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
-import { AuthService, UserDetailsResponse } from "~/services/auth-service/auth-service";
+import { AuthService } from "~/services/auth-service/auth-service";
import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
import * as jsSHA from "jssha";
return null;
};
-const getUserDetails = async (baseUrl: string, token: string): Promise<UserDetailsResponse> => {
- const resp = await Axios.get<UserDetailsResponse>(`${baseUrl}/users/current`, {
- headers: {
- Authorization: `OAuth2 ${token}`
- }
- });
- return resp.data;
-};
-
const invalidV2Token = "Must be a v2 token";
export const getSaltedToken = (clusterId: string, token: string) => {
Promise<{ user: User; token: string }> => {
const saltedToken = getSaltedToken(config.uuidPrefix, useToken);
- const user = await getUserDetails(config.baseUrl, saltedToken);
+
+ const svc = createServices(config, { progressFn: () => { }, errorFn: () => { } });
+ setAuthorizationHeader(svc, saltedToken);
+
+ const user = await svc.authService.getUserDetails();
return {
- user: {
- firstName: user.first_name,
- lastName: user.last_name,
- uuid: user.uuid,
- ownerUuid: user.owner_uuid,
- email: user.email,
- isAdmin: user.is_admin,
- isActive: user.is_active,
- username: user.username,
- prefs: user.prefs
- },
+ user,
token: saltedToken,
};
};
import { ofType, unionize, UnionOf } from '~/common/unionize';
import { Dispatch } from "redux";
-import { AxiosInstance } from "axios";
import { RootState } from "../store";
import { ServiceRepository } from "~/services/services";
import { SshKeyResource } from '~/models/ssh-key';
-import { User, UserResource } from "~/models/user";
+import { User } from "~/models/user";
import { Session } from "~/models/session";
import { Config } from '~/common/config';
-import { initSessions } from "~/store/auth/auth-action-session";
import { cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
import { matchTokenRoute, matchFedTokenRoute } from '~/routes/routes';
-import { AxiosError } from "axios";
+import { createServices, setAuthorizationHeader, removeAuthorizationHeader } from "~/services/services";
export const authActions = unionize({
- SAVE_API_TOKEN: ofType<string>(),
- SAVE_USER: ofType<UserResource>(),
LOGIN: {},
LOGOUT: {},
CONFIG: ofType<{ config: Config }>(),
REMOTE_CLUSTER_CONFIG: ofType<{ config: Config }>(),
});
-export function setAuthorizationHeader(services: ServiceRepository, token: string) {
- services.apiClient.defaults.headers.common = {
- Authorization: `OAuth2 ${token}`
- };
- services.webdavClient.defaults.headers = {
- Authorization: `OAuth2 ${token}`
- };
-}
-
-function removeAuthorizationHeader(client: AxiosInstance) {
- delete client.defaults.headers.common.Authorization;
-}
-
export const initAuth = (config: Config) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
// Cancel any link account ops in progress unless the user has
// just logged in or there has been a successful link operation
};
const init = (config: Config) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const user = services.authService.getUser();
const token = services.authService.getApiToken();
let homeCluster = services.authService.getHomeCluster();
- if (token) {
- setAuthorizationHeader(services, token);
- }
if (homeCluster && !config.remoteHosts[homeCluster]) {
homeCluster = undefined;
}
dispatch(authActions.CONFIG({ config }));
dispatch(authActions.SET_HOME_CLUSTER(config.loginCluster || homeCluster || config.uuidPrefix));
- document.title = `Arvados Workbench (${config.uuidPrefix})`;
- if (token && user) {
- dispatch(authActions.INIT({ user, token }));
- dispatch<any>(initSessions(services.authService, config, user));
- dispatch<any>(getUserDetails()).then((user: User) => {
- dispatch(authActions.INIT({ user, token }));
- if (!user.isActive) {
- services.userService.activate(user.uuid).then((user: User) => {
- dispatch(authActions.INIT({ user, token }));
- });
- }
- }).catch((err: AxiosError) => {
- if (err.response) {
- // Bad token
- if (err.response.status === 401) {
- dispatch<any>(logout());
- }
- }
- });
+
+ if (token && token !== "undefined") {
+ dispatch<any>(saveApiToken(token));
}
};
-export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- services.authService.saveApiToken(token);
- setAuthorizationHeader(services, token);
- dispatch(authActions.SAVE_API_TOKEN(token));
+export const getConfig = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Config => {
+ const state = getState().auth;
+ return state.remoteHostsConfig[state.localCluster];
};
-export const saveUser = (user: UserResource) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- services.authService.saveUser(user);
- dispatch(authActions.SAVE_USER(user));
+export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+ const config = dispatch<any>(getConfig);
+ const svc = createServices(config, { progressFn: () => { }, errorFn: () => { } });
+ setAuthorizationHeader(svc, token);
+ return svc.authService.getUserDetails().then((user: User) => {
+ dispatch(authActions.INIT({ user, token }));
+ });
};
export const login = (uuidPrefix: string, homeCluster: string, loginCluster: string,
}
services.authService.removeApiToken();
services.authService.removeUser();
- removeAuthorizationHeader(services.apiClient);
+ removeAuthorizationHeader(services);
services.authService.logout();
dispatch(authActions.LOGOUT());
};
-export const getUserDetails = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<User> => {
- dispatch(authActions.USER_DETAILS_REQUEST());
- return services.authService.getUserDetails().then(user => {
- services.authService.saveUser(user);
- dispatch(authActions.USER_DETAILS_SUCCESS(user));
- return user;
- });
-};
-
export type AuthAction = UnionOf<typeof authActions>;
});
});
- it('should save api token', () => {
- const initialState = undefined;
-
- const state = reducer(initialState, authActions.SAVE_API_TOKEN("token"));
- expect(state).toEqual({
- apiToken: "token",
- user: undefined,
- sshKeys: [],
- sessions: [],
- homeCluster: "",
- localCluster: "",
- loginCluster: "",
- remoteHosts: {},
- remoteHostsConfig: {}
- });
- });
-
it('should set user details on success fetch', () => {
const initialState = undefined;
// SPDX-License-Identifier: AGPL-3.0
import { authActions, AuthAction } from "./auth-action";
-import { User, UserResource } from "~/models/user";
+import { User } from "~/models/user";
import { ServiceRepository } from "~/services/services";
import { SshKeyResource } from '~/models/ssh-key';
import { Session } from "~/models/session";
export const authReducer = (services: ServiceRepository) => (state = initialState, action: AuthAction) => {
return authActions.match(action, {
- SAVE_API_TOKEN: (token: string) => {
- return { ...state, apiToken: token };
- },
- SAVE_USER: (user: UserResource) => {
- return { ...state, user };
- },
CONFIG: ({ config }) => {
return {
...state,
import { Dispatch } from "redux";
import { RootState } from "~/store/store";
-import { ServiceRepository } from "~/services/services";
+import { ServiceRepository, createServices, setAuthorizationHeader } from "~/services/services";
import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
import { LinkAccountType, AccountToLink, LinkAccountStatus } from "~/models/link-account";
-import { saveApiToken, saveUser } from "~/store/auth/auth-action";
+import { authActions, getConfig } from "~/store/auth/auth-action";
import { unionize, ofType, UnionOf } from '~/common/unionize';
import { UserResource } from "~/models/user";
import { GroupResource } from "~/models/group";
import { LinkAccountPanelError, OriginatingUser } from "./link-account-panel-reducer";
-import { login, logout, setAuthorizationHeader } from "~/store/auth/auth-action";
+import { login, logout } from "~/store/auth/auth-action";
import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
import { WORKBENCH_LOADING_SCREEN } from '~/store/workbench/workbench-actions';
export const switchUser = (user: UserResource, token: string) =>
(dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- dispatch(saveUser(user));
- dispatch(saveApiToken(token));
+ dispatch(authActions.INIT({ user, token }));
};
export const linkFailed = () =>
// Use the token of the user we are getting data for. This avoids any admin/non-admin permissions
// issues since a user will always be able to query the api server for their own user data.
- setAuthorizationHeader(services, linkAccountData.token);
- const savedUserResource = await services.userService.get(linkAccountData.userUuid);
- setAuthorizationHeader(services, curToken);
+ const config = dispatch<any>(getConfig);
+ const svc = createServices(config, { progressFn: () => { }, errorFn: () => { } });
+ setAuthorizationHeader(svc, linkAccountData.token);
+ const savedUserResource = await svc.userService.get(linkAccountData.userUuid);
let params: any;
if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT || linkAccountData.type === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) {
export const navigateToProcessLogs = compose(push, getProcessLogUrl);
export const navigateToRootProject = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const rootProjectUuid = services.authService.getUuid();
- if (rootProjectUuid) {
- dispatch<any>(navigateTo(rootProjectUuid));
+ const usr = getState().auth.user;
+ if (usr) {
+ dispatch<any>(navigateTo(usr.uuid));
}
};
import { History } from "history";
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';
const middlewares: Middleware[] = [
routerMiddleware(history),
thunkMiddleware.withExtraArgument(services),
+ authMiddleware(services),
projectPanelMiddleware,
favoritePanelMiddleware,
trashPanelMiddleware,
import { UserResource } from "~/models/user";
import { getResource } from '~/store/resources/resources';
import { navigateTo, navigateToUsers, navigateToRootProject } from "~/store/navigation/navigation-action";
-import { saveApiToken } from '~/store/auth/auth-action';
+import { authActions } from '~/store/auth/auth-action';
export const USERS_PANEL_ID = 'usersPanel';
export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const { resources } = getState();
const data = getResource<UserResource>(uuid)(resources);
- if (data) {
- services.authService.saveUser(data);
- }
const client = await services.apiClientAuthorizationService.create({ ownerUuid: uuid });
- dispatch<any>(saveApiToken(`v2/${client.uuid}/${client.apiToken}`));
- location.reload();
- dispatch<any>(navigateToRootProject);
+ if (data) {
+ dispatch<any>(authActions.INIT({ user: data, token: `v2/${client.uuid}/${client.apiToken}` }));
+ location.reload();
+ dispatch<any>(navigateToRootProject);
+ }
};
export const openUserCreateDialog = () =>
import { RouteProps } from "react-router";
import * as React from "react";
import { connect, DispatchProp } from "react-redux";
-import { getUserDetails, saveApiToken } from "~/store/auth/auth-action";
+import { saveApiToken } from "~/store/auth/auth-action";
import { getUrlParameter } from "~/common/url";
import { AuthService } from "~/services/auth-service/auth-service";
import { navigateToRootProject, navigateToLinkAccount } from "~/store/navigation/navigation-action";
-import { User } from "~/models/user";
import { Config } from "~/common/config";
-import { initSessions } from "~/store/auth/auth-action-session";
import { getAccountLinkData } from "~/store/link-account-panel/link-account-panel-actions";
interface ApiTokenProps {
const search = this.props.location ? this.props.location.search : "";
const apiToken = getUrlParameter(search, 'api_token');
const loadMainApp = this.props.loadMainApp;
- this.props.dispatch(saveApiToken(apiToken));
- this.props.dispatch<any>(getUserDetails()).then((user: User) => {
- this.props.dispatch(initSessions(this.props.authService, this.props.config, user));
- }).finally(() => {
+ this.props.dispatch<any>(saveApiToken(apiToken)).finally(() => {
if (loadMainApp) {
if (this.props.dispatch(getAccountLinkData())) {
this.props.dispatch(navigateToLinkAccount);