diff --git a/packages/shared-components/src/libs/oauth/helpers.shared.ts b/packages/shared-components/src/libs/oauth/helpers.shared.ts index 2f46dde8..70aa6d4c 100644 --- a/packages/shared-components/src/libs/oauth/helpers.shared.ts +++ b/packages/shared-components/src/libs/oauth/helpers.shared.ts @@ -1,8 +1,9 @@ import qs from 'qs' export interface OAuthResponseData { - access_token?: string + app_token?: string code: string + github_token?: string scope: string[] } diff --git a/packages/shared-components/src/libs/oauth/helpers.web.ts b/packages/shared-components/src/libs/oauth/helpers.web.ts index 61c60838..19d8c489 100644 --- a/packages/shared-components/src/libs/oauth/helpers.web.ts +++ b/packages/shared-components/src/libs/oauth/helpers.web.ts @@ -21,17 +21,18 @@ export const listenForNextMessageData = ( !( e && e.data && - (e.data.oauth || (e.data.access_token || e.data.error)) + (e.data.oauth || + (e.data.app_token || e.data.github_token || e.data.error)) ) ) { return } - const { access_token: accessToken, error } = e.data + const { app_token: appToken, github_token: githubToken, error } = e.data window.removeEventListener('message', handleMessage) - if (accessToken && !error) resolve(e.data) + if (appToken && githubToken && !error) resolve(e.data) else reject( new Error(typeof error === 'string' ? error : 'No token received'), diff --git a/packages/shared-components/src/libs/oauth/index.ts b/packages/shared-components/src/libs/oauth/index.ts index 6b014415..0c738959 100644 --- a/packages/shared-components/src/libs/oauth/index.ts +++ b/packages/shared-components/src/libs/oauth/index.ts @@ -24,7 +24,7 @@ export async function executeOAuth(scopes: string[]) { if (typeof Browser.dismiss === 'function') Browser.dismiss() - if (!(params && params.access_token)) { + if (!(params && params.app_token && params.github_token)) { throw new Error('Login failed: No access token received.') } diff --git a/packages/shared-components/src/libs/oauth/index.web.ts b/packages/shared-components/src/libs/oauth/index.web.ts index a3264ad2..89d12f27 100644 --- a/packages/shared-components/src/libs/oauth/index.web.ts +++ b/packages/shared-components/src/libs/oauth/index.web.ts @@ -28,7 +28,7 @@ export async function executeOAuth(scopes: string[]) { const data = await listenForNextMessageData(popup) // console.log('[OAUTH] Received data:', data) - if (!(data && data.access_token)) { + if (!(data && data.app_token && data.github_token)) { throw new Error('Login failed: No access token received.') } diff --git a/packages/shared-components/src/redux/actions/auth.ts b/packages/shared-components/src/redux/actions/auth.ts index e4416a8c..812c52b7 100644 --- a/packages/shared-components/src/redux/actions/auth.ts +++ b/packages/shared-components/src/redux/actions/auth.ts @@ -4,11 +4,18 @@ import { createErrorAction, } from 'shared-core/dist/utils/helpers/redux' -export function loginRequest(payload: { token: string }) { +export function loginRequest(payload: { + appToken: string + githubToken: string +}) { return createAction('LOGIN_REQUEST', payload) } -export function loginSuccess(payload: { user: User }) { +export function loginSuccess(payload: { + appToken: string + githubToken: string + user: User +}) { return createAction('LOGIN_SUCCESS', payload) } diff --git a/packages/shared-components/src/redux/reducers/auth.ts b/packages/shared-components/src/redux/reducers/auth.ts index c26de547..8ed5c10b 100644 --- a/packages/shared-components/src/redux/reducers/auth.ts +++ b/packages/shared-components/src/redux/reducers/auth.ts @@ -1,39 +1,52 @@ +import _ from 'lodash' +import { REHYDRATE } from 'redux-persist' + import { User } from 'shared-core/dist/types/graphql' import { Reducer } from '../types' export interface State { + appToken: string | null error: object | null + githubToken: string | null isLoggingIn: boolean lastLoginAt: string | null - token: string user: User | null } const initialState: State = { + appToken: null, error: null, + githubToken: null, isLoggingIn: false, lastLoginAt: null, - token: '', user: null, } export const authReducer: Reducer = (state = initialState, action) => { switch (action.type) { + case REHYDRATE as any: + return { + ...(action.payload as any).auth, + ..._.pick(initialState, ['error', 'isLoggingIn']), + } + case 'LOGIN_REQUEST': return { + appToken: action.payload.appToken, error: null, + githubToken: action.payload.githubToken, isLoggingIn: true, lastLoginAt: state.lastLoginAt, - token: action.payload.token, user: state.user, } case 'LOGIN_SUCCESS': return { + appToken: action.payload.appToken || state.appToken, error: null, + githubToken: action.payload.githubToken || state.githubToken, isLoggingIn: false, lastLoginAt: new Date().toISOString(), - token: state.token, user: action.payload.user, } diff --git a/packages/shared-components/src/redux/sagas/auth.ts b/packages/shared-components/src/redux/sagas/auth.ts index 2f41cd17..eac92caf 100644 --- a/packages/shared-components/src/redux/sagas/auth.ts +++ b/packages/shared-components/src/redux/sagas/auth.ts @@ -6,6 +6,7 @@ import { ExtractActionFromActionCreator, GitHubUser, } from 'shared-core/dist/types' +import { User } from 'shared-core/dist/types/graphql' import { GRAPHQL_ENDPOINT } from 'shared-core/dist/utils/constants' import { fromGitHubUser } from '../../api/mappers/user' import * as github from '../../libs/github' @@ -13,47 +14,60 @@ import * as actions from '../actions' import * as selectors from '../selectors' function* onRehydrate() { - const token = yield select(selectors.tokenSelector) - if (token) yield put(actions.loginRequest({ token })) + const appToken = yield select(selectors.appTokenSelector) + const githubToken = yield select(selectors.githubTokenSelector) + if (!(appToken && githubToken)) return + + yield put(actions.loginRequest({ appToken, githubToken })) } function* onLoginRequest( action: ExtractActionFromActionCreator, ) { - github.authenticate(action.payload.token || '') - try { + github.authenticate(action.payload.githubToken || '') + const response: AxiosResponse<{ - data: { me: any } + data: { + login: { + appToken: string + githubToken: string + user: User | null + } | null + } errors?: any[] }> = yield axios.post( GRAPHQL_ENDPOINT, { - query: `query me { - me { - id - nodeId - login - name - avatarUrl - type - bio - publicGistsCount - publicReposCount - privateReposCount - privateGistsCount - followersCount - followingCount - ownedPrivateReposCount - isTwoFactorAuthenticationEnabled - createdAt - updatedAt + query: `query auth { + login { + appToken + githubToken + user { + id + nodeId + login + name + avatarUrl + type + bio + publicGistsCount + publicReposCount + privateReposCount + privateGistsCount + followersCount + followingCount + ownedPrivateReposCount + isTwoFactorAuthenticationEnabled + createdAt + updatedAt + } } }`, }, { headers: { - Authorization: `bearer ${action.payload.token}`, + Authorization: `bearer ${action.payload.appToken}`, }, }, ) @@ -64,9 +78,28 @@ function* onLoginRequest( throw { response } } - if (!(data && data.me && data.me.id)) throw new Error('Invalid response') + if ( + !( + data && + data.login && + data.login.appToken && + data.login.githubToken && + data.login.user && + data.login.user.id + ) + ) { + throw new Error('Invalid response') + } - yield put(actions.loginSuccess({ user: data.me })) + github.authenticate(data.login.githubToken) + + yield put( + actions.loginSuccess({ + appToken: data.login.appToken, + githubToken: data.login.githubToken, + user: data.login.user, + }), + ) return } catch (error) { console.error(error.response) @@ -74,12 +107,7 @@ function* onLoginRequest( if ( error && error.response && - (error.response.status === 401 || - (error.response.data && - Array.isArray(error.response.data.errors) && - error.response.data.errors.some( - (e: any) => e.extensions && e.extensions.code === 'UNAUTHENTICATED', - ))) + (error.response.status >= 200 || error.response.status < 500) ) { yield put(actions.loginFailure(error.response.data)) return @@ -92,12 +120,24 @@ function* onLoginRequest( const user = fromGitHubUser(githubUser) if (!(user && user.id && user.login)) throw new Error('Invalid response') - yield put(actions.loginSuccess({ user })) + yield put( + actions.loginSuccess({ + appToken: action.payload.appToken, + githubToken: action.payload.githubToken, + user, + }), + ) } catch (error) { yield put(actions.loginFailure(error)) } } +function onLoginSuccess( + action: ExtractActionFromActionCreator, +) { + github.authenticate(action.payload.githubToken) +} + function* onLoginFailure( action: ExtractActionFromActionCreator, ) { @@ -113,6 +153,7 @@ export function* authSagas() { yield takeLatest(REHYDRATE, onRehydrate), yield takeLatest('LOGIN_FAILURE', onLoginFailure), yield takeLatest('LOGIN_REQUEST', onLoginRequest), + yield takeLatest('LOGIN_SUCCESS', onLoginSuccess), yield takeLatest('LOGOUT', onLogout), ]) } diff --git a/packages/shared-components/src/redux/selectors/auth.ts b/packages/shared-components/src/redux/selectors/auth.ts index dae645d3..78d49d54 100644 --- a/packages/shared-components/src/redux/selectors/auth.ts +++ b/packages/shared-components/src/redux/selectors/auth.ts @@ -6,7 +6,11 @@ export const errorSelector = (state: RootState) => s(state).error export const isLoggingInSelector = (state: RootState) => s(state).isLoggingIn -export const tokenSelector = (state: RootState) => s(state).token +export const appTokenSelector = (state: RootState) => s(state).appToken + +export const githubTokenSelector = (state: RootState) => s(state).githubToken export const currentUserSelector = (state: RootState) => - tokenSelector(state) ? s(state).user : undefined + appTokenSelector(state) && githubTokenSelector(state) + ? s(state).user + : undefined diff --git a/packages/shared-components/src/screens/LoginScreen.tsx b/packages/shared-components/src/screens/LoginScreen.tsx index 1f980930..2ae73ca6 100644 --- a/packages/shared-components/src/screens/LoginScreen.tsx +++ b/packages/shared-components/src/screens/LoginScreen.tsx @@ -85,7 +85,7 @@ const connectToStore = connect( user: selectors.currentUserSelector(state), }), { - login: actions.loginRequest, + loginRequest: actions.loginRequest, }, ) @@ -107,20 +107,22 @@ export class LoginScreenComponent extends PureComponent< ? ['user', 'repo', 'notifications', 'read:org'] : ['user', 'public_repo', 'notifications', 'read:org'] - let token + let appToken + let githubToken try { const params = await executeOAuth(permissions) - if (!(params && params.access_token)) - throw new Error('No token received.') + appToken = params && params.app_token + githubToken = params && params.github_token - token = params.access_token + if (!(appToken && githubToken)) throw new Error('No token received.') } catch (e) { console.error(e) + if (e.message === 'Canceled' || e.message === 'Timeout') return alert(`Login failed. ${e || ''}`) return } - await this.props.login({ token }) + await this.props.loginRequest({ appToken, githubToken }) } loginWithGitHubPrivateAccess = () => this._loginWithGitHub('github.private')