@@ -11,7 +11,6 @@ import _ from 'lodash'
1111import qs from 'qs'
1212import url from 'url'
1313import { useDispatch } from 'react-redux'
14- import { Alert } from 'react-native'
1514
1615import { constants , tryParseOAuthParams } from '@devhub/core'
1716
@@ -40,9 +39,7 @@ export interface LoginHelpersProviderState {
4039 fullAccessRef : React . MutableRefObject < boolean >
4140 isExecutingOAuth : boolean
4241 isLoggingIn : boolean
43- loginWithGitHub : ( {
44- fullAccess,
45- } ?: {
42+ loginWithGitHub : ( params ?: {
4643 fullAccess ?: boolean | undefined
4744 } ) => Promise < void >
4845 loginWithGitHubPersonalAccessToken : ( ) => Promise < void >
@@ -79,8 +76,15 @@ export function LoginHelpersProvider(props: LoginHelpersProviderProps) {
7976 > ( )
8077
8178 const dispatch = useDispatch ( )
79+ const githubBaseApiUrl = useReduxState ( selectors . githubBaseApiUrlSelector )
8280 const existingAppToken = useReduxState ( selectors . appTokenSelector )
8381 const isLoggingIn = useReduxState ( selectors . isLoggingInSelector )
82+ const loggedGitHubUserId = useReduxState (
83+ ( state ) => selectors . currentGitHubUserSelector ( state ) ?. id ,
84+ )
85+ const loggedGitHubUsername = useReduxState (
86+ selectors . currentGitHubUsernameSelector ,
87+ )
8488 const error = useReduxState ( selectors . authErrorSelector )
8589 const hasGitHubToken = useReduxState (
8690 ( state ) => ! ! selectors . githubTokenSelector ( state ) ,
@@ -129,7 +133,9 @@ export function LoginHelpersProvider(props: LoginHelpersProviderProps) {
129133 const token = await new Promise < string | undefined > ( ( resolveToken ) => {
130134 Dialog . show (
131135 'Personal Access Token' ,
132- 'To have private access, you need to include the "repo" scope. Paste your GitHub token here:' ,
136+ constants . LOCAL_ONLY_PERSONAL_ACCESS_TOKEN
137+ ? 'It will be stored safely on your local device and only be sent directly to GitHub.'
138+ : 'Enable private repository access.' ,
133139 [
134140 {
135141 text : 'Continue' ,
@@ -159,7 +165,7 @@ export function LoginHelpersProvider(props: LoginHelpersProviderProps) {
159165 {
160166 type : 'plain-text' ,
161167 cancelable : true ,
162- placeholder : 'Personal Access Token' ,
168+ placeholder : 'Paste your Personal Access Token here ' ,
163169 defaultValue : '' ,
164170 } ,
165171 )
@@ -179,80 +185,140 @@ export function LoginHelpersProvider(props: LoginHelpersProviderProps) {
179185 const token = await promptForPersonalAcessToken ( )
180186 if ( ! token ) throw new Error ( 'Canceled' )
181187
182- setIsExecutingOAuth ( true )
183- const response = await axios . post (
184- `${ constants . API_BASE_URL } /github/personal/login` ,
185- { token } ,
186- { headers : getDefaultDevHubHeaders ( { appToken : existingAppToken } ) } ,
187- )
188- setIsExecutingOAuth ( false )
189-
190- const appToken = response . data . appToken
191- clearOAuthQueryParams ( )
192-
193- if ( ! appToken ) throw new Error ( 'No app token' )
194-
195- dispatch ( actions . loginRequest ( { appToken } ) )
188+ if ( constants . LOCAL_ONLY_PERSONAL_ACCESS_TOKEN ) {
189+ setIsExecutingOAuth ( true )
190+ setPATLoadingState ( 'adding' )
191+ const response = await axios . get ( `${ githubBaseApiUrl } /user` , {
192+ headers : {
193+ Authorization : `token ${ token } ` ,
194+ } ,
195+ } )
196+ setIsExecutingOAuth ( false )
197+ setPATLoadingState ( undefined )
198+
199+ if ( ! ( response ?. data ?. id && response . data . login ) )
200+ throw new Error ( 'Invalid response' )
201+
202+ if (
203+ loggedGitHubUserId &&
204+ `${ response . data . id } ` !== `${ loggedGitHubUserId } `
205+ ) {
206+ const details =
207+ response . data . login !== loggedGitHubUsername
208+ ? ` (${ response . data . login } instead of ${ loggedGitHubUsername } )`
209+ : ` (ID ${ response . data . id } instead of ${ loggedGitHubUserId } )`
210+
211+ throw new Error (
212+ `This Personal Access Token seems to be from a different user${ details } .` ,
213+ )
214+ }
215+
216+ const scope = `${ response . headers [ 'x-oauth-scopes' ] || '' } `
217+ . replace ( / \s + / g, '' )
218+ . split ( ',' )
219+ . filter ( Boolean )
220+
221+ if ( scope . length && ! scope . includes ( 'repo' ) ) {
222+ throw new Error (
223+ 'You didn\'t include the "repo" permission scope,' +
224+ ' which is required to have access to private repositories.' +
225+ " Your token will be safe on your device, and will never be sent to DevHub's server." ,
226+ )
227+ }
228+
229+ dispatch (
230+ actions . replacePersonalTokenDetails ( {
231+ tokenDetails : {
232+ login : response . data . login ,
233+ token,
234+ tokenCreatedAt : new Date ( ) . toISOString ( ) ,
235+ scope,
236+ tokenType : undefined ,
237+ } ,
238+ } ) ,
239+ )
240+ } else {
241+ setIsExecutingOAuth ( true )
242+ setPATLoadingState ( 'adding' )
243+ const response = await axios . post (
244+ `${ constants . API_BASE_URL } /github/personal/login` ,
245+ { token } ,
246+ { headers : getDefaultDevHubHeaders ( { appToken : existingAppToken } ) } ,
247+ )
248+ setIsExecutingOAuth ( false )
249+ setPATLoadingState ( undefined )
250+
251+ const appToken = response . data . appToken
252+ clearOAuthQueryParams ( )
253+
254+ if ( ! appToken ) throw new Error ( 'No app token' )
255+
256+ dispatch ( actions . loginRequest ( { appToken } ) )
257+ }
196258 } catch ( error ) {
197259 setIsExecutingOAuth ( false )
260+ setPATLoadingState ( undefined )
261+
198262 if ( error . message === 'Canceled' || error . message === 'Timeout' ) return
199263
200- const description = 'OAuth execution failed'
264+ const description = 'Authentication failed'
201265 console . error ( description , error )
202266
203267 bugsnag . notify ( error , { description } )
204268
205269 Dialog . show ( 'Login failed' , `${ error || '' } ` )
206270 }
207- } , [ existingAppToken ] )
271+ } , [ existingAppToken , loggedGitHubUserId , loggedGitHubUsername ] )
208272
209273 const addPersonalAccessToken = useCallback ( async ( ) => {
210- setPATLoadingState ( 'adding' )
211274 await loginWithGitHubPersonalAccessToken ( )
212- setPATLoadingState ( undefined )
213275 } , [ loginWithGitHubPersonalAccessToken ] )
214276
215277 const removePersonalAccessToken = useCallback ( async ( ) => {
216- try {
217- setPATLoadingState ( 'removing' )
278+ if ( constants . LOCAL_ONLY_PERSONAL_ACCESS_TOKEN ) {
279+ dispatch (
280+ actions . replacePersonalTokenDetails ( {
281+ tokenDetails : undefined ,
282+ } ) ,
283+ )
284+ } else {
285+ try {
286+ setPATLoadingState ( 'removing' )
218287
219- const response = await axios . post (
220- constants . GRAPHQL_ENDPOINT ,
221- {
222- query : `
288+ const response = await axios . post (
289+ constants . GRAPHQL_ENDPOINT ,
290+ {
291+ query : `
223292 mutation {
224293 removeGitHubPersonalToken
225294 }` ,
226- } ,
227- { headers : getDefaultDevHubHeaders ( { appToken : existingAppToken } ) } ,
228- )
229-
230- const { data, errors } = await response . data
295+ } ,
296+ { headers : getDefaultDevHubHeaders ( { appToken : existingAppToken } ) } ,
297+ )
231298
232- if ( errors && errors [ 0 ] && errors [ 0 ] . message )
233- throw new Error ( errors [ 0 ] . message )
299+ const { data, errors } = await response . data
234300
235- if ( ! ( data && data . removeGitHubPersonalToken ) ) {
236- throw new Error ( 'Not removed.' )
237- }
301+ if ( errors ?. [ 0 ] ?. message ) throw new Error ( errors [ 0 ] . message )
238302
239- setPATLoadingState ( undefined )
303+ if ( ! data ?. removeGitHubPersonalToken ) {
304+ throw new Error ( 'Not removed.' )
305+ }
240306
241- // dispatch(
242- // actions.replacePersonalTokenDetails({
243- // tokenDetails: undefined,
244- // }),
245- // )
307+ setPATLoadingState ( undefined )
246308
247- // this is only necessary because we are not re-generating the appToken after removing the personal token,
248- // which causes the personal token to being added back after a page refresh
249- dispatch ( actions . logout ( ) )
250- } catch ( error ) {
251- console . error ( error )
252- bugsnag . notify ( error )
309+ // this is only necessary because we are not re-generating the appToken after removing the personal token,
310+ // which causes the personal token to being added back after a page refresh
311+ dispatch ( actions . logout ( ) )
312+ } catch ( error ) {
313+ console . error ( error )
314+ bugsnag . notify ( error )
253315
254- setPATLoadingState ( undefined )
255- Alert . alert ( `Failed to remove personal token. \nError: ${ error . message } ` )
316+ setPATLoadingState ( undefined )
317+ Dialog . show (
318+ 'Failed to remove personal token' ,
319+ `Error: ${ error ?. message } ` ,
320+ )
321+ }
256322 }
257323 } , [ existingAppToken ] )
258324
@@ -281,7 +347,7 @@ export function LoginHelpersProvider(props: LoginHelpersProviderProps) {
281347 if ( error . message === 'Canceled' || error . message === 'Timeout' ) return
282348 bugsnag . notify ( error , { description } )
283349
284- Dialog . show ( 'Login failed' , `${ error || '' } ` )
350+ Dialog . show ( 'Login failed' , `Error: ${ error ?. message } ` )
285351 }
286352 } , [ ] )
287353
0 commit comments