Skip to content

Commit 1dd2ac5

Browse files
sumomomomomolinedoestrolling
authored andcommitted
Update Google Drive Login Buttons UI + Use session for login persistence
1 parent c9efdfa commit 1dd2ac5

File tree

9 files changed

+81
-41
lines changed

9 files changed

+81
-41
lines changed

src/commons/application/ApplicationTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,7 @@ export const defaultSession: SessionState = {
513513
assessmentOverviews: undefined,
514514
agreedToResearch: undefined,
515515
sessionId: Date.now(),
516+
googleAccessToken: undefined,
516517
githubOctokitObject: { octokit: undefined },
517518
gradingOverviews: undefined,
518519
students: undefined,

src/commons/application/actions/SessionActions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
LOGIN,
5252
LOGIN_GITHUB,
5353
LOGOUT_GITHUB,
54+
LOGIN_GOOGLE,
5455
LOGOUT_GOOGLE,
5556
NotificationConfiguration,
5657
NotificationPreference,
@@ -64,6 +65,7 @@ import {
6465
SET_COURSE_REGISTRATION,
6566
SET_GITHUB_ACCESS_TOKEN,
6667
SET_GITHUB_OCTOKIT_OBJECT,
68+
SET_GOOGLE_ACCESS_TOKEN,
6769
SET_GOOGLE_USER,
6870
SET_NOTIFICATION_CONFIGS,
6971
SET_TOKENS,
@@ -159,6 +161,8 @@ export const fetchStudents = createAction(FETCH_STUDENTS, () => ({ payload: {} }
159161

160162
export const login = createAction(LOGIN, (providerId: string) => ({ payload: providerId }));
161163

164+
export const loginGoogle = createAction(LOGIN_GOOGLE, () => ({ payload: {} }));
165+
162166
export const logoutGoogle = createAction(LOGOUT_GOOGLE, () => ({ payload: {} }));
163167

164168
export const loginGitHub = createAction(LOGIN_GITHUB, () => ({ payload: {} }));
@@ -203,6 +207,9 @@ export const setAdminPanelCourseRegistrations = createAction(
203207

204208
export const setGoogleUser = createAction(SET_GOOGLE_USER, (user?: string) => ({ payload: user }));
205209

210+
export const setGoogleAccessToken =
211+
createAction(SET_GOOGLE_ACCESS_TOKEN, (accessToken?: string) => ({ payload: accessToken}));
212+
206213
export const setGitHubOctokitObject = createAction(
207214
SET_GITHUB_OCTOKIT_OBJECT,
208215
(authToken?: string) => ({ payload: generateOctokitInstance(authToken || '') })

src/commons/application/reducers/SessionsReducer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
SET_GITHUB_ACCESS_TOKEN,
1919
SET_GITHUB_OCTOKIT_OBJECT,
2020
SET_GOOGLE_USER,
21+
SET_GOOGLE_ACCESS_TOKEN,
2122
SET_NOTIFICATION_CONFIGS,
2223
SET_TOKENS,
2324
SET_USER,
@@ -54,6 +55,11 @@ export const SessionsReducer: Reducer<SessionState, SourceActionType> = (
5455
...state,
5556
googleUser: action.payload
5657
};
58+
case SET_GOOGLE_ACCESS_TOKEN:
59+
return {
60+
...state,
61+
googleAccessToken: action.payload
62+
}
5763
case SET_TOKENS:
5864
return {
5965
...state,

src/commons/application/types/SessionTypes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const FETCH_STUDENTS = 'FETCH_STUDENTS';
3131
export const FETCH_TEAM_FORMATION_OVERVIEW = 'FETCH_TEAM_FORMATION_OVERVIEW';
3232
export const FETCH_TEAM_FORMATION_OVERVIEWS = 'FETCH_TEAM_FORMATION_OVERVIEWS';
3333
export const LOGIN = 'LOGIN';
34+
export const LOGIN_GOOGLE = 'LOGIN_GOOGLE';
3435
export const LOGOUT_GOOGLE = 'LOGOUT_GOOGLE';
3536
export const LOGIN_GITHUB = 'LOGIN_GITHUB';
3637
export const LOGOUT_GITHUB = 'LOGOUT_GITHUB';
@@ -41,6 +42,7 @@ export const SET_COURSE_REGISTRATION = 'SET_COURSE_REGISTRATION';
4142
export const SET_ASSESSMENT_CONFIGURATIONS = 'SET_ASSESSMENT_CONFIGURATIONS';
4243
export const SET_ADMIN_PANEL_COURSE_REGISTRATIONS = 'SET_ADMIN_PANEL_COURSE_REGISTRATIONS';
4344
export const SET_GOOGLE_USER = 'SET_GOOGLE_USER';
45+
export const SET_GOOGLE_ACCESS_TOKEN = 'SET_GOOGLE_ACCESS_TOKEN';
4446
export const SET_GITHUB_OCTOKIT_OBJECT = 'SET_GITHUB_OCTOKIT_OBJECT';
4547
export const SET_GITHUB_ACCESS_TOKEN = 'SET_GITHUB_ACCESS_TOKEN';
4648
export const SUBMIT_ANSWER = 'SUBMIT_ANSWER';
@@ -132,6 +134,7 @@ export type SessionState = {
132134
readonly gradings: Map<number, GradingQuery>;
133135
readonly notifications: Notification[];
134136
readonly googleUser?: string;
137+
readonly googleAccessToken?: string;
135138
readonly githubOctokitObject: { octokit: Octokit | undefined };
136139
readonly githubAccessToken?: string;
137140
readonly remoteExecutionDevices?: Device[];

src/commons/controlBar/ControlBarGoogleDriveButtons.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Props = {
2222
onClickSave?: () => any;
2323
onClickSaveAs?: () => any;
2424
onClickLogOut?: () => any;
25+
onClickLogIn?: () => any;
2526
onPopoverOpening?: () => any;
2627
};
2728

@@ -41,7 +42,12 @@ export const ControlBarGoogleDriveButtons: React.FC<Props> = props => {
4142
/>
4243
);
4344
const openButton = (
44-
<ControlButton label="Open" icon={IconNames.DOCUMENT_OPEN} onClick={props.onClickOpen} />
45+
<ControlButton
46+
label="Open"
47+
icon={IconNames.DOCUMENT_OPEN}
48+
onClick={props.onClickOpen}
49+
isDisabled={props.loggedInAs ? false : true}
50+
/>
4551
);
4652
const saveButton = (
4753
<ControlButton
@@ -53,13 +59,22 @@ export const ControlBarGoogleDriveButtons: React.FC<Props> = props => {
5359
/>
5460
);
5561
const saveAsButton = (
56-
<ControlButton label="Save as" icon={IconNames.SEND_TO} onClick={props.onClickSaveAs} />
62+
<ControlButton
63+
label="Save as"
64+
icon={IconNames.SEND_TO}
65+
onClick={props.onClickSaveAs}
66+
isDisabled={props.loggedInAs ? false : true}
67+
/>
5768
);
58-
const logoutButton = props.loggedInAs && (
69+
70+
const loginButton = props.loggedInAs ? (
5971
<Tooltip2 content={`Logged in as ${props.loggedInAs}`}>
60-
<ControlButton label="Log out" icon={IconNames.LOG_OUT} onClick={props.onClickLogOut} />
72+
<ControlButton label="Log Out" icon={IconNames.LOG_OUT} onClick={props.onClickLogOut} />
6173
</Tooltip2>
74+
) : (
75+
<ControlButton label="Log In" icon={IconNames.LOG_IN} onClick={props.onClickLogIn} />
6276
);
77+
6378
const tooltipContent = props.isFolderModeEnabled
6479
? 'Currently unsupported in Folder mode'
6580
: undefined;
@@ -74,7 +89,7 @@ export const ControlBarGoogleDriveButtons: React.FC<Props> = props => {
7489
{openButton}
7590
{saveButton}
7691
{saveAsButton}
77-
{logoutButton}
92+
{loginButton}
7893
</ButtonGroup>
7994
</div>
8095
}

src/commons/sagas/PersistenceSaga.tsx

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { store } from '../../pages/createStore';
1414
import { OverallState } from '../application/ApplicationTypes';
1515
import { ExternalLibraryName } from '../application/types/ExternalTypes';
16-
import { LOGOUT_GOOGLE } from '../application/types/SessionTypes';
16+
import { LOGIN_GOOGLE, LOGOUT_GOOGLE } from '../application/types/SessionTypes';
1717
import { actions } from '../utils/ActionsHelper';
1818
import Constants from '../utils/Constants';
1919
import { showSimpleConfirmDialog, showSimplePromptDialog } from '../utils/DialogHelper';
@@ -43,9 +43,13 @@ export function* persistenceSaga(): SagaIterator {
4343
yield takeLatest(LOGOUT_GOOGLE, function* () {
4444
yield put(actions.playgroundUpdatePersistenceFile(undefined));
4545
yield call(ensureInitialised);
46-
yield gapi.client.setToken(null);
47-
yield handleUserChanged(null);
48-
yield localStorage.removeItem("gsi-access-token");
46+
yield call([gapi.client, "setToken"], null);
47+
yield call(handleUserChanged, null);
48+
});
49+
50+
yield takeLatest(LOGIN_GOOGLE, function* () {
51+
yield call(ensureInitialised);
52+
yield call(getToken);
4953
});
5054

5155
yield takeLatest(PERSISTENCE_OPEN_PICKER, function* (): any {
@@ -311,31 +315,35 @@ const initialisationPromise: Promise<void> = new Promise(res => {
311315
startInitialisation = res;
312316
}).then(initialise);
313317

314-
async function getUserProfileData(accessToken: string) {
318+
/**
319+
* Calls Google useinfo API to get user's profile data, specifically email, using accessToken.
320+
* If email field does not exist in the JSON response (invalid access token), will return undefined.
321+
* Used with handleUserChanged to handle login/logout.
322+
* @param accessToken GIS access token
323+
* @returns string if email field exists in JSON response, undefined if not
324+
*/
325+
async function getUserProfileDataEmail(accessToken: string): Promise<string | undefined> {
315326
const headers = new Headers();
316327
headers.append('Authorization', `Bearer ${accessToken}`);
317328
const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
318329
headers
319330
});
320331
const data = await response.json();
321-
return data;
332+
return data.email;
322333
}
323334

324-
async function isOAuthTokenValid(accessToken: string) {
325-
const userProfileData = await getUserProfileData(accessToken);
326-
return userProfileData.error ? false : true;
327-
}
328-
329-
// updates store and localStorage
335+
// only function that updates store
330336
async function handleUserChanged(accessToken: string | null) {
331337
// logs out if null
332-
if (accessToken === null) {
338+
if (accessToken === null) { // clear store
333339
store.dispatch(actions.setGoogleUser(undefined));
340+
store.dispatch(actions.setGoogleAccessToken(undefined));
334341
} else {
335-
const userProfileData = await getUserProfileData(accessToken)
336-
const email = userProfileData.email;
342+
const email = await getUserProfileDataEmail(accessToken);
343+
// if access token is invalid, const email will be undefined
344+
// so stores will also be cleared here if it is invalid
337345
store.dispatch(actions.setGoogleUser(email));
338-
localStorage.setItem("gsi-access-token", accessToken);
346+
store.dispatch(actions.setGoogleAccessToken(accessToken));
339347
}
340348
}
341349

@@ -377,24 +385,24 @@ async function initialise() { // only called once
377385
});
378386

379387
// check for stored token
380-
// if it exists and is valid, load manually
381-
// leave checking whether it is valid or not to ensureInitialisedAndAuthorised
382-
if (localStorage.getItem("gsi-access-token")) {
383-
await loadToken(localStorage.getItem("gsi-access-token")!);
388+
const accessToken = store.getState().session.googleAccessToken;
389+
if (accessToken) {
390+
gapi.client.setToken({access_token: accessToken});
391+
handleUserChanged(accessToken); // this also logs out user if stored token is invalid
384392
}
385393
}
386394

387395
// adapted from https://developers.google.com/identity/oauth2/web/guides/migration-to-gis
388-
async function getToken() {
389-
await new Promise((resolve, reject) => {
396+
function* getToken() {
397+
yield new Promise((resolve, reject) => {
390398
try {
391399
// Settle this promise in the response callback for requestAccessToken()
392-
// as any used here cos of limitations of the type declaration library
393400
(tokenClient as any).callback = (resp: google.accounts.oauth2.TokenResponse) => {
394401
if (resp.error !== undefined) {
395402
reject(resp);
396403
}
397-
// GIS has automatically updated gapi.client with the newly issued access token.
404+
// GIS has already automatically updated gapi.client
405+
// with the newly issued access token by this point
398406
handleUserChanged(gapi.client.getToken().access_token);
399407
resolve(resp);
400408
};
@@ -405,27 +413,21 @@ async function getToken() {
405413
});
406414
}
407415

408-
// manually load token, when token is not gotten from getToken()
409-
// but instead from localStorage
410-
async function loadToken(accessToken: string) {
411-
gapi.client.setToken({access_token: accessToken});
412-
return handleUserChanged(accessToken);
413-
}
414-
415416
function* ensureInitialised() {
416417
startInitialisation();
417418
yield initialisationPromise;
418419
}
419420

420421
function* ensureInitialisedAndAuthorised() { // called multiple times
421422
yield call(ensureInitialised);
422-
const currToken = gapi.client.getToken();
423+
const currToken: GoogleApiOAuth2TokenObject = yield call(gapi.client.getToken);
423424

424425
if (currToken === null) {
425426
yield call(getToken);
426427
} else {
427428
// check if loaded token is still valid
428-
const isValid: boolean = yield call(isOAuthTokenValid, currToken.access_token);
429+
const email: string | undefined = yield call(getUserProfileDataEmail, currToken.access_token);
430+
const isValid = email ? true : false;
429431
if (!isValid) {
430432
yield call(getToken);
431433
}

src/pages/createStore.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ function loadStore(loadedStore: SavedState | undefined) {
5353
octokit: loadedStore.session.githubAccessToken
5454
? generateOctokitInstance(loadedStore.session.githubAccessToken)
5555
: undefined
56-
}
56+
},
57+
googleUser: loadedStore.session.googleAccessToken
58+
? 'placeholder'
59+
: undefined
5760
},
5861
workspaces: {
5962
...defaultState.workspaces,

src/pages/localStorage.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ export const saveState = (state: OverallState) => {
7474
assessmentConfigurations: state.session.assessmentConfigurations,
7575
notificationConfigs: state.session.notificationConfigs,
7676
configurableNotificationConfigs: state.session.configurableNotificationConfigs,
77-
githubAccessToken: state.session.githubAccessToken
77+
githubAccessToken: state.session.githubAccessToken,
78+
googleAccessToken: state.session.googleAccessToken
7879
},
7980
achievements: state.achievement.achievements,
8081
playgroundIsFolderModeEnabled: state.workspaces.playground.isFolderModeEnabled,

src/pages/playground/Playground.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import {
2121
loginGitHub,
2222
logoutGitHub,
23+
loginGoogle,
2324
logoutGoogle
2425
} from 'src/commons/application/actions/SessionActions';
2526
import {
@@ -271,7 +272,7 @@ const Playground: React.FC<PlaygroundProps> = props => {
271272
googleUser: persistenceUser,
272273
githubOctokitObject
273274
} = useTypedSelector(state => state.session);
274-
275+
275276
const dispatch = useDispatch();
276277
const {
277278
handleChangeExecTime,
@@ -596,6 +597,7 @@ const Playground: React.FC<PlaygroundProps> = props => {
596597
onClickSave={
597598
persistenceFile ? () => dispatch(persistenceSaveFile(persistenceFile)) : undefined
598599
}
600+
onClickLogIn={() => dispatch(loginGoogle())}
599601
onClickLogOut={() => dispatch(logoutGoogle())}
600602
onPopoverOpening={() => dispatch(persistenceInitialise())}
601603
/>

0 commit comments

Comments
 (0)