15088: Updates state to account for both types of linking and adds UI
authorEric Biagiotti <ebiagiotti@veritasgenetics.com>
Mon, 29 Apr 2019 19:28:58 +0000 (15:28 -0400)
committerEric Biagiotti <ebiagiotti@veritasgenetics.com>
Mon, 29 Apr 2019 19:28:58 +0000 (15:28 -0400)
- Simplifies user type in the link-account-panel to only be a UserResource, and subsequently removes createdAt from the auth user.
- Switches users back to the originating user when account linking is cancelled after logging on with the second account.

Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti@veritasgenetics.com

src/models/link-account.ts
src/models/user.ts
src/services/auth-service/auth-service.ts
src/store/auth/auth-action-session.ts
src/store/auth/auth-action.ts
src/store/auth/auth-reducer.test.ts
src/store/auth/auth-reducer.ts
src/store/link-account-panel/link-account-panel-actions.ts
src/store/link-account-panel/link-account-panel-reducer.ts
src/views/link-account-panel/link-account-panel-root.tsx
src/views/link-account-panel/link-account-panel.tsx

index dd22bc95e90db60a28a12a62bb0bda043aeb0ef6..8932a3352f39275f5bb3360a72171587f489f0f3 100644 (file)
@@ -9,5 +9,6 @@ export enum LinkAccountType {
 
 export interface AccountToLink {
     type: LinkAccountType;
-    userToken: string;
+    userUuid: string;
+    token: string;
 }
index afc4fc72265ea62c03e9b55e436fcc074ef475bf..2497864507787cef09d6d3914780b79136090d77 100644 (file)
@@ -24,7 +24,6 @@ export interface User {
     prefs: UserPrefs;
     isAdmin: boolean;
     isActive: boolean;
-    createdAt: string;
 }
 
 export const getUserFullname = (user?: User) => {
index e7641fdca9cd5bb1ad4efb9ea200bdd65d452356..eae219dd0ad2a547883b94047a961d10048f5019 100644 (file)
@@ -20,7 +20,6 @@ export const USER_IS_ADMIN = 'isAdmin';
 export const USER_IS_ACTIVE = 'isActive';
 export const USER_USERNAME = 'username';
 export const USER_PREFS = 'prefs';
-export const USER_CREATED_AT = 'createdAt';
 
 export interface UserDetailsResponse {
     email: string;
@@ -31,7 +30,6 @@ export interface UserDetailsResponse {
     is_admin: boolean;
     is_active: boolean;
     username: string;
-    created_at: string;
     prefs: UserPrefs;
 }
 
@@ -79,11 +77,10 @@ export class AuthService {
         const isAdmin = this.getIsAdmin();
         const isActive = this.getIsActive();
         const username = localStorage.getItem(USER_USERNAME);
-        const createdAt = localStorage.getItem(USER_CREATED_AT);
         const prefs = JSON.parse(localStorage.getItem(USER_PREFS) || '{"profile": {}}');
 
-        return email && firstName && lastName && uuid && ownerUuid && username && createdAt && prefs
-            ? { email, firstName, lastName, uuid, ownerUuid, isAdmin, isActive, username, createdAt, prefs }
+        return email && firstName && lastName && uuid && ownerUuid && username && prefs
+            ? { email, firstName, lastName, uuid, ownerUuid, isAdmin, isActive, username, prefs }
             : undefined;
     }
 
@@ -96,7 +93,6 @@ export class AuthService {
         localStorage.setItem(USER_IS_ADMIN, JSON.stringify(user.isAdmin));
         localStorage.setItem(USER_IS_ACTIVE, JSON.stringify(user.isActive));
         localStorage.setItem(USER_USERNAME, user.username);
-        localStorage.setItem(USER_CREATED_AT, user.createdAt);
         localStorage.setItem(USER_PREFS, JSON.stringify(user.prefs));
     }
 
@@ -109,7 +105,6 @@ export class AuthService {
         localStorage.removeItem(USER_IS_ADMIN);
         localStorage.removeItem(USER_IS_ACTIVE);
         localStorage.removeItem(USER_USERNAME);
-        localStorage.removeItem(USER_CREATED_AT);
         localStorage.removeItem(USER_PREFS);
     }
 
@@ -140,7 +135,6 @@ export class AuthService {
                     isAdmin: resp.data.is_admin,
                     isActive: resp.data.is_active,
                     username: resp.data.username,
-                    createdAt: resp.data.created_at,
                     prefs
                 };
             })
index 0a41fe58f66ffb0b57008d56ccb8d45ac8ec20b4..5bb192b8816e6c4fc7bff110424ff7ad83617d02 100644 (file)
@@ -94,7 +94,6 @@ const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: S
             isAdmin: user.is_admin,
             isActive: user.is_active,
             username: user.username,
-            createdAt: user.created_at,
             prefs: user.prefs
         },
         token: saltedToken
index baf80595f300ad9fb7d181aa8ba5441303e71aab..bd6852807459a0790cd254923a83726ba04ac523 100644 (file)
@@ -8,13 +8,15 @@ import { AxiosInstance } from "axios";
 import { RootState } from "../store";
 import { ServiceRepository } from "~/services/services";
 import { SshKeyResource } from '~/models/ssh-key';
-import { User } from "~/models/user";
+import { User, UserResource } from "~/models/user";
 import { Session } from "~/models/session";
 import { Config } from '~/common/config';
 import { initSessions } from "~/store/auth/auth-action-session";
+import { UserRepositoryCreate } from '~/views-components/dialog-create/dialog-user-create';
 
 export const authActions = unionize({
     SAVE_API_TOKEN: ofType<string>(),
+    SAVE_USER: ofType<UserResource>(),
     LOGIN: {},
     LOGOUT: {},
     CONFIG: ofType<{ config: Config }>(),
@@ -66,6 +68,11 @@ export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: ()
     dispatch(authActions.SAVE_API_TOKEN(token));
 };
 
+export const saveUser = (user: UserResource) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    services.authService.saveUser(user);
+    dispatch(authActions.SAVE_USER(user));
+};
+
 export const login = (uuidPrefix: string, homeCluster: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
     services.authService.login(uuidPrefix, homeCluster);
     dispatch(authActions.LOGIN());
index e008818234e23a5fc5069a857711ecdfad813271..38cf1581d3796dc5b89a4d7cfc897adc425271ce 100644 (file)
@@ -33,8 +33,7 @@ describe('auth-reducer', () => {
             username: "username",
             prefs: {},
             isAdmin: false,
-            isActive: true,
-            createdAt: "createdAt"
+            isActive: true
         };
         const state = reducer(initialState, authActions.INIT({ user, token: "token" }));
         expect(state).toEqual({
@@ -75,8 +74,7 @@ describe('auth-reducer', () => {
             username: "username",
             prefs: {},
             isAdmin: false,
-            isActive: true,
-            createdAt: "createdAt"
+            isActive: true
         };
 
         const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
@@ -96,8 +94,7 @@ describe('auth-reducer', () => {
                 username: "username",
                 prefs: {},
                 isAdmin: false,
-                isActive: true,
-                createdAt: "createdAt"
+                isActive: true
             }
         });
     });
index 0335752678c56421336751dfd79eaef11316db94..c87fc5ddb15320eb89357022870c7a649b87ab0f 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { authActions, AuthAction } from "./auth-action";
-import { User } from "~/models/user";
+import { User, UserResource } from "~/models/user";
 import { ServiceRepository } from "~/services/services";
 import { SshKeyResource } from '~/models/ssh-key';
 import { Session } from "~/models/session";
@@ -33,6 +33,9 @@ export const authReducer = (services: ServiceRepository) => (state = initialStat
         SAVE_API_TOKEN: (token: string) => {
             return { ...state, apiToken: token };
         },
+        SAVE_USER: (user: UserResource) => {
+            return { ...state, user};
+        },
         CONFIG: ({ config }) => {
             return {
                 ...state,
index dd3a79e93193f659d61206d8bd4de9af57736664..42fd6c52481d0b286d412f5f10411f95c74b684b 100644 (file)
@@ -7,11 +7,13 @@ import { RootState } from "~/store/store";
 import { ServiceRepository } from "~/services/services";
 import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
 import { LinkAccountType, AccountToLink } from "~/models/link-account";
-import { logout } from "~/store/auth/auth-action";
+import { logout, saveApiToken, saveUser } from "~/store/auth/auth-action";
 import { unionize, ofType, UnionOf } from '~/common/unionize';
+import { UserResource } from "~/models/user";
+import { navigateToRootProject } from "~/store/navigation/navigation-action";
 
 export const linkAccountPanelActions = unionize({
-    LOAD_LINKING: ofType<AccountToLink>(),
+    LOAD_LINKING: ofType<{ user: UserResource | undefined, userToLink: UserResource | undefined }>(),
     REMOVE_LINKING: {}
 });
 
@@ -21,15 +23,33 @@ export const loadLinkAccountPanel = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         dispatch(setBreadcrumbs([{ label: 'Link account'}]));
 
-        const linkAccountData = services.linkAccountService.getLinkAccount();
-        if (linkAccountData) {
-            dispatch(linkAccountPanelActions.LOAD_LINKING(linkAccountData));
+        const curUser = getState().auth.user;
+        if (curUser) {
+            services.userService.get(curUser.uuid).then(curUserResource => {
+                const linkAccountData = services.linkAccountService.getLinkAccount();
+                if (linkAccountData) {
+                    services.userService.get(linkAccountData.userUuid).then(savedUserResource => {
+                        if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
+                            dispatch<any>(linkAccountPanelActions.LOAD_LINKING({ userToLink: curUserResource, user: savedUserResource }));
+                        }
+                        else if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
+                            dispatch<any>(linkAccountPanelActions.LOAD_LINKING({ userToLink: savedUserResource, user: curUserResource }));
+                        }
+                        else {
+                            throw new Error('Invalid link account type.');
+                        }
+                    });
+                }
+                else {
+                    dispatch<any>(linkAccountPanelActions.LOAD_LINKING({ userToLink: undefined, user: curUserResource }));
+                }
+            });
         }
     };
 
 export const saveAccountLinkData = (t: LinkAccountType) =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const accountToLink = {type: t, userToken: services.authService.getApiToken()} as AccountToLink;
+        const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
         services.linkAccountService.saveLinkAccount(accountToLink);
         dispatch(logout());
     };
@@ -41,6 +61,19 @@ export const getAccountLinkData = () =>
 
 export const removeAccountLinkData = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        const linkAccountData = services.linkAccountService.getLinkAccount();
         services.linkAccountService.removeLinkAccount();
         dispatch(linkAccountPanelActions.REMOVE_LINKING());
+        if (linkAccountData) {
+            services.userService.get(linkAccountData.userUuid).then(savedUser => {
+                dispatch(setBreadcrumbs([{ label: ''}]));
+                dispatch<any>(saveUser(savedUser));
+                dispatch<any>(saveApiToken(linkAccountData.token));
+                dispatch<any>(navigateToRootProject);
+            });
+        }
+    };
+
+export const linkAccount = () =>
+    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
     };
\ No newline at end of file
index 47718e3ba4905d9ed16923106d1e590ff7beba3d..cbfb315b200c193342fe0a01bd0de9879b896c67 100644 (file)
@@ -3,19 +3,21 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { linkAccountPanelActions, LinkAccountPanelAction } from "~/store/link-account-panel/link-account-panel-actions";
-import { AccountToLink } from "~/models/link-account";
+import { UserResource, User } from "~/models/user";
 
 export interface LinkAccountPanelState {
-    accountToLink: AccountToLink | undefined;
+    user: UserResource | undefined;
+    userToLink: UserResource | undefined;
 }
 
 const initialState = {
-    accountToLink: undefined
+    user: undefined,
+    userToLink: undefined
 };
 
 export const linkAccountPanelReducer = (state: LinkAccountPanelState = initialState, action: LinkAccountPanelAction) =>
     linkAccountPanelActions.match(action, {
         default: () => state,
-        LOAD_LINKING: (accountToLink) => ({ ...state, accountToLink }),
-        REMOVE_LINKING: () => ({...state, accountToLink: undefined})
+        LOAD_LINKING: ({ userToLink, user }) => ({ ...state, user, userToLink }),
+        REMOVE_LINKING: () => ({ ...state, user: undefined, userToLink: undefined })
     });
\ No newline at end of file
index 4b3b6979f79cc42624d6d6acc9d19e4abe54c002..8b3fb45a607209b512339d7f3f6c5d7de65a8527 100644 (file)
@@ -14,7 +14,7 @@ import {
     Grid,
 } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { User } from "~/models/user";
+import { User, UserResource } from "~/models/user";
 import { LinkAccountType, AccountToLink } from "~/models/link-account";
 import { formatDate }from "~/common/formatters";
 
@@ -28,25 +28,35 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 });
 
 export interface LinkAccountPanelRootDataProps {
-    user?: User;
-    accountToLink?: AccountToLink;
+    user?: UserResource;
+    userToLink?: UserResource;
 }
 
 export interface LinkAccountPanelRootActionProps {
     saveAccountLinkData: (type: LinkAccountType) => void;
     removeAccountLinkData: () => void;
+    linkAccount: () => void;
+}
+
+function displayUser(user: UserResource, showCreatedAt: boolean = false) {
+    const disp = [];
+    disp.push(<span><b>{user.email}</b> ({user.username}, {user.uuid})</span>);
+    if (showCreatedAt) {
+        disp.push(<span> created on <b>{formatDate(user.createdAt)}</b></span>);
+    }
+    return disp;
 }
 
 type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
 
 export const LinkAccountPanelRoot = withStyles(styles) (
-    ({classes, user, accountToLink, saveAccountLinkData, removeAccountLinkData}: LinkAccountPanelRootProps) => {
+    ({classes, user, userToLink, saveAccountLinkData, removeAccountLinkData, linkAccount}: LinkAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
-            { user && accountToLink===undefined && <Grid container spacing={24}>
+            { user && userToLink===undefined && <Grid container spacing={24}>
                 <Grid container item direction="column" spacing={24}>
                     <Grid item>
-                        You are currently logged in as <b>{user.email}</b> ({user.username}, {user.uuid}) created on <b>{formatDate(user.createdAt)}</b>
+                        You are currently logged in as {displayUser(user, true)}
                     </Grid>
                     <Grid item>
                         You can link Arvados accounts. After linking, either login will take you to the same account.
@@ -65,7 +75,18 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                     </Grid>
                 </Grid>
             </Grid>}
-            { accountToLink && <Grid container spacing={24}>
+            { userToLink && user && <Grid container spacing={24}>
+                <Grid container item direction="column" spacing={24}>
+                    <Grid item>
+                        Clicking 'Link accounts' will link {displayUser(userToLink, true)} to {displayUser(user, true)}.
+                    </Grid>
+                    <Grid item>
+                        After linking, logging in as {displayUser(userToLink)} will log you into the same account as {displayUser(user)}.
+                    </Grid>
+                    <Grid item>
+                       Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(user)}.
+                    </Grid>
+                </Grid>
                 <Grid container item direction="row" spacing={24}>
                     <Grid item>
                         <Button variant="contained" onClick={() => removeAccountLinkData()}>
@@ -73,12 +94,12 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                         </Button>
                     </Grid>
                     <Grid item>
-                        <Button color="primary" variant="contained" onClick={() => {}}>
+                        <Button color="primary" variant="contained" onClick={() => linkAccount()}>
                             Link accounts
                         </Button>
                     </Grid>
                 </Grid>
             </Grid> }
             </CardContent>
-        </Card>;
+        </Card> ;
 });
\ No newline at end of file
index 44b4bb549c23d82b326b5fcc023ffa7a56964929..498eff881e1563430dadc70111160f90b477c26b 100644 (file)
@@ -5,7 +5,7 @@
 import { RootState } from '~/store/store';
 import { Dispatch } from 'redux';
 import { connect } from 'react-redux';
-import { saveAccountLinkData, removeAccountLinkData } from '~/store/link-account-panel/link-account-panel-actions';
+import { saveAccountLinkData, removeAccountLinkData, linkAccount } from '~/store/link-account-panel/link-account-panel-actions';
 import { LinkAccountType } from '~/models/link-account';
 import {
     LinkAccountPanelRoot,
@@ -15,14 +15,15 @@ import {
 
 const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
     return {
-        user: state.auth.user,
-        accountToLink: state.linkAccountPanel.accountToLink
+        user: state.linkAccountPanel.user,
+        userToLink: state.linkAccountPanel.userToLink
     };
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): LinkAccountPanelRootActionProps => ({
     saveAccountLinkData: (type: LinkAccountType) => dispatch<any>(saveAccountLinkData(type)),
-    removeAccountLinkData: () => dispatch<any>(removeAccountLinkData())
+    removeAccountLinkData: () => dispatch<any>(removeAccountLinkData()),
+    linkAccount: () => dispatch<any>(linkAccount())
 });
 
 export const LinkAccountPanel = connect(mapStateToProps, mapDispatchToProps)(LinkAccountPanelRoot);