@jbrowse/plugin-authentication 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/dist/DropboxOAuthModel/configSchema.d.ts +5 -0
- package/dist/DropboxOAuthModel/configSchema.js +69 -0
- package/dist/DropboxOAuthModel/configSchema.js.map +1 -0
- package/dist/DropboxOAuthModel/index.d.ts +2 -0
- package/dist/DropboxOAuthModel/index.js +11 -0
- package/dist/DropboxOAuthModel/index.js.map +1 -0
- package/dist/DropboxOAuthModel/model.d.ts +98 -0
- package/dist/DropboxOAuthModel/model.js +103 -0
- package/dist/DropboxOAuthModel/model.js.map +1 -0
- package/dist/ExternalTokenModel/ExternalTokenEntryForm.d.ts +5 -0
- package/dist/ExternalTokenModel/ExternalTokenEntryForm.js +59 -0
- package/dist/ExternalTokenModel/ExternalTokenEntryForm.js.map +1 -0
- package/dist/ExternalTokenModel/configSchema.d.ts +5 -0
- package/dist/ExternalTokenModel/configSchema.js +26 -0
- package/dist/ExternalTokenModel/configSchema.js.map +1 -0
- package/dist/ExternalTokenModel/index.d.ts +2 -0
- package/dist/ExternalTokenModel/index.js +11 -0
- package/dist/ExternalTokenModel/index.js.map +1 -0
- package/dist/ExternalTokenModel/model.d.ts +67 -0
- package/dist/ExternalTokenModel/model.js +59 -0
- package/dist/ExternalTokenModel/model.js.map +1 -0
- package/dist/GoogleDriveOAuthModel/configSchema.d.ts +5 -0
- package/dist/GoogleDriveOAuthModel/configSchema.js +53 -0
- package/dist/GoogleDriveOAuthModel/configSchema.js.map +1 -0
- package/dist/GoogleDriveOAuthModel/index.d.ts +2 -0
- package/dist/GoogleDriveOAuthModel/index.js +11 -0
- package/dist/GoogleDriveOAuthModel/index.js.map +1 -0
- package/dist/GoogleDriveOAuthModel/model.d.ts +111 -0
- package/dist/GoogleDriveOAuthModel/model.js +115 -0
- package/dist/GoogleDriveOAuthModel/model.js.map +1 -0
- package/dist/HTTPBasicModel/HTTPBasicLoginForm.d.ts +5 -0
- package/dist/HTTPBasicModel/HTTPBasicLoginForm.js +55 -0
- package/dist/HTTPBasicModel/HTTPBasicLoginForm.js.map +1 -0
- package/dist/HTTPBasicModel/configSchema.d.ts +5 -0
- package/dist/HTTPBasicModel/configSchema.js +34 -0
- package/dist/HTTPBasicModel/configSchema.js.map +1 -0
- package/dist/HTTPBasicModel/index.d.ts +2 -0
- package/dist/HTTPBasicModel/index.js +11 -0
- package/dist/HTTPBasicModel/index.js.map +1 -0
- package/dist/HTTPBasicModel/model.d.ts +67 -0
- package/dist/HTTPBasicModel/model.js +59 -0
- package/dist/HTTPBasicModel/model.js.map +1 -0
- package/dist/OAuthModel/configSchema.d.ts +5 -0
- package/dist/OAuthModel/configSchema.js +90 -0
- package/dist/OAuthModel/configSchema.js.map +1 -0
- package/dist/OAuthModel/index.d.ts +2 -0
- package/dist/OAuthModel/index.js +11 -0
- package/dist/OAuthModel/index.js.map +1 -0
- package/dist/OAuthModel/model.d.ts +91 -0
- package/dist/OAuthModel/model.js +317 -0
- package/dist/OAuthModel/model.js.map +1 -0
- package/dist/index.d.ts +399 -0
- package/dist/index.js +80 -0
- package/dist/index.js.map +1 -0
- package/esm/DropboxOAuthModel/configSchema.d.ts +5 -0
- package/esm/DropboxOAuthModel/configSchema.js +64 -0
- package/esm/DropboxOAuthModel/configSchema.js.map +1 -0
- package/esm/DropboxOAuthModel/index.d.ts +2 -0
- package/esm/DropboxOAuthModel/index.js +3 -0
- package/esm/DropboxOAuthModel/index.js.map +1 -0
- package/esm/DropboxOAuthModel/model.d.ts +98 -0
- package/esm/DropboxOAuthModel/model.js +96 -0
- package/esm/DropboxOAuthModel/model.js.map +1 -0
- package/esm/ExternalTokenModel/ExternalTokenEntryForm.d.ts +5 -0
- package/esm/ExternalTokenModel/ExternalTokenEntryForm.js +29 -0
- package/esm/ExternalTokenModel/ExternalTokenEntryForm.js.map +1 -0
- package/esm/ExternalTokenModel/configSchema.d.ts +5 -0
- package/esm/ExternalTokenModel/configSchema.js +24 -0
- package/esm/ExternalTokenModel/configSchema.js.map +1 -0
- package/esm/ExternalTokenModel/index.d.ts +2 -0
- package/esm/ExternalTokenModel/index.js +3 -0
- package/esm/ExternalTokenModel/index.js.map +1 -0
- package/esm/ExternalTokenModel/model.d.ts +67 -0
- package/esm/ExternalTokenModel/model.js +57 -0
- package/esm/ExternalTokenModel/model.js.map +1 -0
- package/esm/GoogleDriveOAuthModel/configSchema.d.ts +5 -0
- package/esm/GoogleDriveOAuthModel/configSchema.js +48 -0
- package/esm/GoogleDriveOAuthModel/configSchema.js.map +1 -0
- package/esm/GoogleDriveOAuthModel/index.d.ts +2 -0
- package/esm/GoogleDriveOAuthModel/index.js +3 -0
- package/esm/GoogleDriveOAuthModel/index.js.map +1 -0
- package/esm/GoogleDriveOAuthModel/model.d.ts +111 -0
- package/esm/GoogleDriveOAuthModel/model.js +108 -0
- package/esm/GoogleDriveOAuthModel/model.js.map +1 -0
- package/esm/HTTPBasicModel/HTTPBasicLoginForm.d.ts +5 -0
- package/esm/HTTPBasicModel/HTTPBasicLoginForm.js +28 -0
- package/esm/HTTPBasicModel/HTTPBasicLoginForm.js.map +1 -0
- package/esm/HTTPBasicModel/configSchema.d.ts +5 -0
- package/esm/HTTPBasicModel/configSchema.js +32 -0
- package/esm/HTTPBasicModel/configSchema.js.map +1 -0
- package/esm/HTTPBasicModel/index.d.ts +2 -0
- package/esm/HTTPBasicModel/index.js +3 -0
- package/esm/HTTPBasicModel/index.js.map +1 -0
- package/esm/HTTPBasicModel/model.d.ts +67 -0
- package/esm/HTTPBasicModel/model.js +57 -0
- package/esm/HTTPBasicModel/model.js.map +1 -0
- package/esm/OAuthModel/configSchema.d.ts +5 -0
- package/esm/OAuthModel/configSchema.js +88 -0
- package/esm/OAuthModel/configSchema.js.map +1 -0
- package/esm/OAuthModel/index.d.ts +2 -0
- package/esm/OAuthModel/index.js +3 -0
- package/esm/OAuthModel/index.js.map +1 -0
- package/esm/OAuthModel/model.d.ts +91 -0
- package/esm/OAuthModel/model.js +289 -0
- package/esm/OAuthModel/model.js.map +1 -0
- package/esm/index.d.ts +399 -0
- package/esm/index.js +64 -0
- package/esm/index.js.map +1 -0
- package/package.json +63 -0
- package/src/DropboxOAuthModel/configSchema.ts +77 -0
- package/src/DropboxOAuthModel/index.ts +2 -0
- package/src/DropboxOAuthModel/model.tsx +141 -0
- package/src/ExternalTokenModel/ExternalTokenEntryForm.tsx +61 -0
- package/src/ExternalTokenModel/configSchema.ts +36 -0
- package/src/ExternalTokenModel/index.ts +2 -0
- package/src/ExternalTokenModel/model.tsx +70 -0
- package/src/GoogleDriveOAuthModel/configSchema.ts +61 -0
- package/src/GoogleDriveOAuthModel/index.ts +2 -0
- package/src/GoogleDriveOAuthModel/model.tsx +174 -0
- package/src/HTTPBasicModel/HTTPBasicLoginForm.tsx +71 -0
- package/src/HTTPBasicModel/configSchema.ts +43 -0
- package/src/HTTPBasicModel/index.ts +2 -0
- package/src/HTTPBasicModel/model.tsx +70 -0
- package/src/OAuthModel/configSchema.ts +98 -0
- package/src/OAuthModel/index.ts +2 -0
- package/src/OAuthModel/model.tsx +357 -0
- package/src/__snapshots__/index.test.js.snap +8 -0
- package/src/index.test.js +96 -0
- package/src/index.ts +97 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { ConfigurationReference } from '@jbrowse/core/configuration'
|
|
3
|
+
import { UriLocation } from '@jbrowse/core/util/types'
|
|
4
|
+
import { SvgIconProps, SvgIcon } from '@mui/material'
|
|
5
|
+
import { Instance, types } from 'mobx-state-tree'
|
|
6
|
+
import { DropboxOAuthInternetAccountConfigModel } from './configSchema'
|
|
7
|
+
import baseModel from '../OAuthModel/model'
|
|
8
|
+
import { configSchema as OAuthConfigSchema } from '../OAuthModel'
|
|
9
|
+
|
|
10
|
+
interface DropboxError {
|
|
11
|
+
error_summary: string
|
|
12
|
+
error: {
|
|
13
|
+
'.tag': string
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Error messages from https://www.dropbox.com/developers/documentation/http/documentation#sharing-get_shared_link_file */
|
|
18
|
+
const dropboxErrorMessages: Record<string, string | undefined> = {
|
|
19
|
+
shared_link_not_found: "The shared link wasn't found.",
|
|
20
|
+
shared_link_access_denied:
|
|
21
|
+
'The caller is not allowed to access this shared link.',
|
|
22
|
+
unsupported_link_type:
|
|
23
|
+
'This type of link is not supported; use files/export instead.',
|
|
24
|
+
shared_link_is_directory: 'Directories cannot be retrieved by this endpoint.',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function DropboxIcon(props: SvgIconProps) {
|
|
28
|
+
return (
|
|
29
|
+
<SvgIcon {...props}>
|
|
30
|
+
<path d="M3 6.2L8 9.39L13 6.2L8 3L3 6.2M13 6.2L18 9.39L23 6.2L18 3L13 6.2M3 12.55L8 15.74L13 12.55L8 9.35L3 12.55M18 9.35L13 12.55L18 15.74L23 12.55L18 9.35M8.03 16.8L13.04 20L18.04 16.8L13.04 13.61L8.03 16.8Z" />
|
|
31
|
+
</SvgIcon>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function getDescriptiveErrorMessage(response: Response) {
|
|
36
|
+
let errorMessage
|
|
37
|
+
try {
|
|
38
|
+
errorMessage = await response.text()
|
|
39
|
+
} catch (error) {
|
|
40
|
+
errorMessage = ''
|
|
41
|
+
}
|
|
42
|
+
if (errorMessage) {
|
|
43
|
+
let errorMessageParsed: DropboxError | undefined
|
|
44
|
+
try {
|
|
45
|
+
errorMessageParsed = JSON.parse(errorMessage)
|
|
46
|
+
} catch (error) {
|
|
47
|
+
errorMessageParsed = undefined
|
|
48
|
+
}
|
|
49
|
+
if (errorMessageParsed) {
|
|
50
|
+
const messageTag = errorMessageParsed.error['.tag']
|
|
51
|
+
errorMessage = dropboxErrorMessages[messageTag] || messageTag
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return `Network response failure — ${response.status} (${
|
|
55
|
+
response.statusText
|
|
56
|
+
})${errorMessage ? ` (${errorMessage})` : ''}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const stateModelFactory = (
|
|
60
|
+
configSchema: DropboxOAuthInternetAccountConfigModel,
|
|
61
|
+
) => {
|
|
62
|
+
return baseModel(OAuthConfigSchema)
|
|
63
|
+
.named('DropboxOAuthInternetAccount')
|
|
64
|
+
.props({
|
|
65
|
+
type: types.literal('DropboxOAuthInternetAccount'),
|
|
66
|
+
configuration: ConfigurationReference(configSchema),
|
|
67
|
+
})
|
|
68
|
+
.views(() => ({
|
|
69
|
+
get toggleContents() {
|
|
70
|
+
return <DropboxIcon />
|
|
71
|
+
},
|
|
72
|
+
get selectorLabel() {
|
|
73
|
+
return 'Enter Dropbox share link'
|
|
74
|
+
},
|
|
75
|
+
}))
|
|
76
|
+
.actions(self => ({
|
|
77
|
+
getFetcher(
|
|
78
|
+
location?: UriLocation,
|
|
79
|
+
): (input: RequestInfo, init?: RequestInit) => Promise<Response> {
|
|
80
|
+
return async (
|
|
81
|
+
input: RequestInfo,
|
|
82
|
+
init?: RequestInit,
|
|
83
|
+
): Promise<Response> => {
|
|
84
|
+
const authToken = await self.getToken(location)
|
|
85
|
+
const newInit = self.addAuthHeaderToInit(
|
|
86
|
+
{ ...init, method: 'POST' },
|
|
87
|
+
authToken,
|
|
88
|
+
)
|
|
89
|
+
newInit.headers.append(
|
|
90
|
+
'Dropbox-API-Arg',
|
|
91
|
+
JSON.stringify({ url: input }),
|
|
92
|
+
)
|
|
93
|
+
const response = await fetch(
|
|
94
|
+
'https://content.dropboxapi.com/2/sharing/get_shared_link_file',
|
|
95
|
+
newInit,
|
|
96
|
+
)
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const message = await getDescriptiveErrorMessage(response)
|
|
99
|
+
throw new Error(message)
|
|
100
|
+
}
|
|
101
|
+
return response
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
async validateToken(
|
|
105
|
+
token: string,
|
|
106
|
+
location: UriLocation,
|
|
107
|
+
): Promise<string> {
|
|
108
|
+
const response = await fetch(
|
|
109
|
+
'https://api.dropboxapi.com/2/sharing/get_shared_link_metadata',
|
|
110
|
+
{
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {
|
|
113
|
+
Authorization: `Bearer ${token}`,
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
url: location.uri,
|
|
118
|
+
}),
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
const refreshToken =
|
|
123
|
+
self.hasRefreshToken && self.retrieveRefreshToken()
|
|
124
|
+
if (refreshToken) {
|
|
125
|
+
self.removeRefreshToken()
|
|
126
|
+
const newToken = await self.exchangeRefreshForAccessToken(
|
|
127
|
+
refreshToken,
|
|
128
|
+
)
|
|
129
|
+
return this.validateToken(newToken, location)
|
|
130
|
+
}
|
|
131
|
+
const message = await getDescriptiveErrorMessage(response)
|
|
132
|
+
throw new Error(`Token could not be validated. ${message}`)
|
|
133
|
+
}
|
|
134
|
+
return token
|
|
135
|
+
},
|
|
136
|
+
}))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export default stateModelFactory
|
|
140
|
+
export type DropboxOAuthStateModel = ReturnType<typeof stateModelFactory>
|
|
141
|
+
export type DropboxOAuthModel = Instance<DropboxOAuthStateModel>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import Button from '@mui/material/Button'
|
|
3
|
+
import Dialog from '@mui/material/Dialog'
|
|
4
|
+
import DialogContent from '@mui/material/DialogContent'
|
|
5
|
+
import DialogTitle from '@mui/material/DialogTitle'
|
|
6
|
+
import DialogActions from '@mui/material/DialogActions'
|
|
7
|
+
import TextField from '@mui/material/TextField'
|
|
8
|
+
|
|
9
|
+
export const ExternalTokenEntryForm = ({
|
|
10
|
+
internetAccountId,
|
|
11
|
+
handleClose,
|
|
12
|
+
}: {
|
|
13
|
+
internetAccountId: string
|
|
14
|
+
handleClose: (token?: string) => void
|
|
15
|
+
}) => {
|
|
16
|
+
const [token, setToken] = useState('')
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<Dialog open maxWidth="xl" data-testid="externalToken-form">
|
|
21
|
+
<DialogTitle>Enter Token for {internetAccountId}</DialogTitle>
|
|
22
|
+
<DialogContent style={{ display: 'flex', flexDirection: 'column' }}>
|
|
23
|
+
<TextField
|
|
24
|
+
required
|
|
25
|
+
label="Enter Token"
|
|
26
|
+
variant="outlined"
|
|
27
|
+
inputProps={{ 'data-testid': 'entry-externalToken' }}
|
|
28
|
+
onChange={event => {
|
|
29
|
+
setToken(event.target.value)
|
|
30
|
+
}}
|
|
31
|
+
margin="dense"
|
|
32
|
+
/>
|
|
33
|
+
</DialogContent>
|
|
34
|
+
<DialogActions>
|
|
35
|
+
<Button
|
|
36
|
+
variant="contained"
|
|
37
|
+
color="primary"
|
|
38
|
+
type="submit"
|
|
39
|
+
disabled={!token}
|
|
40
|
+
onClick={() => {
|
|
41
|
+
if (token) {
|
|
42
|
+
handleClose(token)
|
|
43
|
+
}
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
Add
|
|
47
|
+
</Button>
|
|
48
|
+
<Button
|
|
49
|
+
variant="contained"
|
|
50
|
+
color="primary"
|
|
51
|
+
onClick={() => {
|
|
52
|
+
handleClose()
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
Cancel
|
|
56
|
+
</Button>
|
|
57
|
+
</DialogActions>
|
|
58
|
+
</Dialog>
|
|
59
|
+
</>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ConfigurationSchema } from '@jbrowse/core/configuration'
|
|
2
|
+
import { Instance } from 'mobx-state-tree'
|
|
3
|
+
import { BaseInternetAccountConfig } from '@jbrowse/core/pluggableElementTypes/models'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* #config ExternalTokenInternetAccount
|
|
7
|
+
*/
|
|
8
|
+
function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
9
|
+
|
|
10
|
+
const ExternalTokenConfigSchema = ConfigurationSchema(
|
|
11
|
+
'ExternalTokenInternetAccount',
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* #slot
|
|
15
|
+
*/
|
|
16
|
+
validateWithHEAD: {
|
|
17
|
+
description: 'validate the token with a HEAD request before using it',
|
|
18
|
+
type: 'boolean',
|
|
19
|
+
defaultValue: true,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
/**
|
|
24
|
+
* #baseConfiguration
|
|
25
|
+
*/
|
|
26
|
+
baseConfiguration: BaseInternetAccountConfig,
|
|
27
|
+
explicitlyTyped: true,
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
export type ExternalTokenInternetAccountConfigModel =
|
|
32
|
+
typeof ExternalTokenConfigSchema
|
|
33
|
+
|
|
34
|
+
export type ExternalTokenInternetAccountConfig =
|
|
35
|
+
Instance<ExternalTokenInternetAccountConfigModel>
|
|
36
|
+
export default ExternalTokenConfigSchema
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ConfigurationReference, getConf } from '@jbrowse/core/configuration'
|
|
2
|
+
import { InternetAccount } from '@jbrowse/core/pluggableElementTypes/models'
|
|
3
|
+
import { UriLocation } from '@jbrowse/core/util/types'
|
|
4
|
+
import { ExternalTokenInternetAccountConfigModel } from './configSchema'
|
|
5
|
+
import { Instance, types, getRoot } from 'mobx-state-tree'
|
|
6
|
+
|
|
7
|
+
import { ExternalTokenEntryForm } from './ExternalTokenEntryForm'
|
|
8
|
+
|
|
9
|
+
const stateModelFactory = (
|
|
10
|
+
configSchema: ExternalTokenInternetAccountConfigModel,
|
|
11
|
+
) => {
|
|
12
|
+
return InternetAccount.named('ExternalTokenInternetAccount')
|
|
13
|
+
.props({
|
|
14
|
+
type: types.literal('ExternalTokenInternetAccount'),
|
|
15
|
+
configuration: ConfigurationReference(configSchema),
|
|
16
|
+
})
|
|
17
|
+
.views(self => ({
|
|
18
|
+
get validateWithHEAD(): boolean {
|
|
19
|
+
return getConf(self, 'validateWithHEAD')
|
|
20
|
+
},
|
|
21
|
+
}))
|
|
22
|
+
.actions(self => ({
|
|
23
|
+
getTokenFromUser(
|
|
24
|
+
resolve: (token: string) => void,
|
|
25
|
+
reject: (error: Error) => void,
|
|
26
|
+
) {
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
const { session } = getRoot<any>(self)
|
|
29
|
+
session.queueDialog((doneCallback: () => void) => [
|
|
30
|
+
ExternalTokenEntryForm,
|
|
31
|
+
{
|
|
32
|
+
internetAccountId: self.internetAccountId,
|
|
33
|
+
handleClose: (token: string) => {
|
|
34
|
+
if (token) {
|
|
35
|
+
resolve(token)
|
|
36
|
+
} else {
|
|
37
|
+
reject(new Error('user cancelled entry'))
|
|
38
|
+
}
|
|
39
|
+
doneCallback()
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
])
|
|
43
|
+
},
|
|
44
|
+
async validateToken(token: string, location: UriLocation) {
|
|
45
|
+
if (!self.validateWithHEAD) {
|
|
46
|
+
return token
|
|
47
|
+
}
|
|
48
|
+
const newInit = self.addAuthHeaderToInit({ method: 'HEAD' }, token)
|
|
49
|
+
const response = await fetch(location.uri, newInit)
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
let errorMessage
|
|
52
|
+
try {
|
|
53
|
+
errorMessage = await response.text()
|
|
54
|
+
} catch (error) {
|
|
55
|
+
errorMessage = ''
|
|
56
|
+
}
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Token could not be validated — ${response.status} (${
|
|
59
|
+
response.statusText
|
|
60
|
+
})${errorMessage ? ` (${errorMessage})` : ''}`,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
return token
|
|
64
|
+
},
|
|
65
|
+
}))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default stateModelFactory
|
|
69
|
+
export type ExternalTokenStateModel = ReturnType<typeof stateModelFactory>
|
|
70
|
+
export type ExternalTokenModel = Instance<ExternalTokenStateModel>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ConfigurationSchema } from '@jbrowse/core/configuration'
|
|
2
|
+
import { Instance } from 'mobx-state-tree'
|
|
3
|
+
import OAuthConfigSchema from '../OAuthModel/configSchema'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* #config GoogleDriveOAuthInternetAccount
|
|
7
|
+
*/
|
|
8
|
+
function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
9
|
+
|
|
10
|
+
const GoogleDriveOAuthConfigSchema = ConfigurationSchema(
|
|
11
|
+
'GoogleDriveOAuthInternetAccount',
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* #slot
|
|
15
|
+
*/
|
|
16
|
+
authEndpoint: {
|
|
17
|
+
description: 'the authorization code endpoint of the internet account',
|
|
18
|
+
type: 'string',
|
|
19
|
+
defaultValue: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
20
|
+
},
|
|
21
|
+
/**
|
|
22
|
+
* #slot
|
|
23
|
+
*/
|
|
24
|
+
scopes: {
|
|
25
|
+
description: 'optional scopes for the authorization call',
|
|
26
|
+
type: 'string',
|
|
27
|
+
defaultValue: 'https://www.googleapis.com/auth/drive.readonly',
|
|
28
|
+
},
|
|
29
|
+
/**
|
|
30
|
+
* #slot
|
|
31
|
+
*/
|
|
32
|
+
domains: {
|
|
33
|
+
description:
|
|
34
|
+
'array of valid domains the url can contain to use this account',
|
|
35
|
+
type: 'stringArray',
|
|
36
|
+
defaultValue: ['drive.google.com"'],
|
|
37
|
+
},
|
|
38
|
+
/**
|
|
39
|
+
* #slot
|
|
40
|
+
*/
|
|
41
|
+
responseType: {
|
|
42
|
+
description: 'the type of response from the authorization endpoint',
|
|
43
|
+
type: 'string',
|
|
44
|
+
defaultValue: 'token',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
/**
|
|
49
|
+
* #baseConfiguration
|
|
50
|
+
*/
|
|
51
|
+
baseConfiguration: OAuthConfigSchema,
|
|
52
|
+
explicitlyTyped: true,
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
export type GoogleDriveOAuthInternetAccountConfigModel =
|
|
57
|
+
typeof GoogleDriveOAuthConfigSchema
|
|
58
|
+
|
|
59
|
+
export type GoogleDriveOAuthInternetAccountConfig =
|
|
60
|
+
Instance<GoogleDriveOAuthInternetAccountConfigModel>
|
|
61
|
+
export default GoogleDriveOAuthConfigSchema
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { ConfigurationReference } from '@jbrowse/core/configuration'
|
|
3
|
+
import { Instance, types } from 'mobx-state-tree'
|
|
4
|
+
import { RemoteFileWithRangeCache } from '@jbrowse/core/util/io'
|
|
5
|
+
import { UriLocation } from '@jbrowse/core/util/types'
|
|
6
|
+
import { SvgIconProps, SvgIcon } from '@mui/material'
|
|
7
|
+
import {
|
|
8
|
+
FilehandleOptions,
|
|
9
|
+
Stats,
|
|
10
|
+
PolyfilledResponse,
|
|
11
|
+
} from 'generic-filehandle'
|
|
12
|
+
|
|
13
|
+
// locals
|
|
14
|
+
import { GoogleDriveOAuthInternetAccountConfigModel } from './configSchema'
|
|
15
|
+
import baseModel from '../OAuthModel/model'
|
|
16
|
+
import { configSchema as OAuthConfigSchema } from '../OAuthModel'
|
|
17
|
+
|
|
18
|
+
export interface RequestInitWithMetadata extends RequestInit {
|
|
19
|
+
metadataOnly?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface GoogleDriveFilehandleOptions extends FilehandleOptions {
|
|
23
|
+
fetch(
|
|
24
|
+
input: RequestInfo,
|
|
25
|
+
opts?: RequestInitWithMetadata,
|
|
26
|
+
): Promise<PolyfilledResponse>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface GoogleDriveError {
|
|
30
|
+
error: {
|
|
31
|
+
errors: {
|
|
32
|
+
domain: string
|
|
33
|
+
reason: string
|
|
34
|
+
message: string
|
|
35
|
+
locationType?: string
|
|
36
|
+
location?: string
|
|
37
|
+
}[]
|
|
38
|
+
code: number
|
|
39
|
+
message: string
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class GoogleDriveFile extends RemoteFileWithRangeCache {
|
|
44
|
+
private statsPromise: Promise<{ size: number }>
|
|
45
|
+
constructor(source: string, opts: GoogleDriveFilehandleOptions) {
|
|
46
|
+
super(source, opts)
|
|
47
|
+
this.statsPromise = this.fetch(source, {
|
|
48
|
+
metadataOnly: true,
|
|
49
|
+
}).then((response: Response) => response.json())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async fetch(
|
|
53
|
+
input: RequestInfo,
|
|
54
|
+
opts?: RequestInitWithMetadata,
|
|
55
|
+
): Promise<PolyfilledResponse> {
|
|
56
|
+
return super.fetch(input, opts)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async stat(): Promise<Stats> {
|
|
60
|
+
return this.statsPromise
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function GoogleDriveIcon(props: SvgIconProps) {
|
|
65
|
+
return (
|
|
66
|
+
<SvgIcon {...props}>
|
|
67
|
+
<path d="M7.71,3.5L1.15,15L4.58,21L11.13,9.5M9.73,15L6.3,21H19.42L22.85,15M22.28,14L15.42,2H8.58L8.57,2L15.43,14H22.28Z" />
|
|
68
|
+
</SvgIcon>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function getDescriptiveErrorMessage(response: Response) {
|
|
73
|
+
let errorMessage
|
|
74
|
+
try {
|
|
75
|
+
errorMessage = await response.text()
|
|
76
|
+
} catch (error) {
|
|
77
|
+
errorMessage = ''
|
|
78
|
+
}
|
|
79
|
+
if (errorMessage) {
|
|
80
|
+
let errorMessageParsed: GoogleDriveError | undefined
|
|
81
|
+
try {
|
|
82
|
+
errorMessageParsed = JSON.parse(errorMessage)
|
|
83
|
+
} catch (error) {
|
|
84
|
+
errorMessageParsed = undefined
|
|
85
|
+
}
|
|
86
|
+
if (errorMessageParsed) {
|
|
87
|
+
errorMessage = errorMessageParsed.error.message
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return `Network response failure — ${response.status} (${
|
|
91
|
+
response.statusText
|
|
92
|
+
})${errorMessage ? ` (${errorMessage})` : ''}`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const stateModelFactory = (
|
|
96
|
+
configSchema: GoogleDriveOAuthInternetAccountConfigModel,
|
|
97
|
+
) => {
|
|
98
|
+
return baseModel(OAuthConfigSchema)
|
|
99
|
+
.named('GoogleDriveOAuthInternetAccount')
|
|
100
|
+
.props({
|
|
101
|
+
type: types.literal('GoogleDriveOAuthInternetAccount'),
|
|
102
|
+
configuration: ConfigurationReference(configSchema),
|
|
103
|
+
})
|
|
104
|
+
.views(() => ({
|
|
105
|
+
get toggleContents() {
|
|
106
|
+
return <GoogleDriveIcon />
|
|
107
|
+
},
|
|
108
|
+
get selectorLabel() {
|
|
109
|
+
return 'Enter Google Drive share link'
|
|
110
|
+
},
|
|
111
|
+
}))
|
|
112
|
+
.actions(self => ({
|
|
113
|
+
getFetcher(
|
|
114
|
+
location?: UriLocation,
|
|
115
|
+
): (input: RequestInfo, init?: RequestInit) => Promise<Response> {
|
|
116
|
+
return async (
|
|
117
|
+
input: RequestInfo,
|
|
118
|
+
init?: RequestInitWithMetadata,
|
|
119
|
+
): Promise<Response> => {
|
|
120
|
+
const urlId = String(input).match(/[-\w]{25,}/)
|
|
121
|
+
const driveUrl = new URL(
|
|
122
|
+
`https://www.googleapis.com/drive/v3/files/${urlId}`,
|
|
123
|
+
)
|
|
124
|
+
const searchParams = new URLSearchParams()
|
|
125
|
+
if (init?.metadataOnly) {
|
|
126
|
+
searchParams.append('fields', 'size')
|
|
127
|
+
} else {
|
|
128
|
+
searchParams.append('alt', 'media')
|
|
129
|
+
}
|
|
130
|
+
driveUrl.search = searchParams.toString()
|
|
131
|
+
const authToken = await self.getToken(location)
|
|
132
|
+
const newInit = self.addAuthHeaderToInit(
|
|
133
|
+
{ ...init, method: 'GET', credentials: 'same-origin' },
|
|
134
|
+
authToken,
|
|
135
|
+
)
|
|
136
|
+
const response = await fetch(driveUrl.toString(), newInit)
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
const message = await getDescriptiveErrorMessage(response)
|
|
139
|
+
throw new Error(message)
|
|
140
|
+
}
|
|
141
|
+
return response
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
openLocation(location: UriLocation) {
|
|
145
|
+
return new GoogleDriveFile(location.uri, {
|
|
146
|
+
fetch: this.getFetcher(location),
|
|
147
|
+
})
|
|
148
|
+
},
|
|
149
|
+
async validateToken(
|
|
150
|
+
token: string,
|
|
151
|
+
location: UriLocation,
|
|
152
|
+
): Promise<string> {
|
|
153
|
+
const urlId = location.uri.match(/[-\w]{25,}/)
|
|
154
|
+
const response = await fetch(
|
|
155
|
+
`https://www.googleapis.com/drive/v3/files/${urlId}`,
|
|
156
|
+
{
|
|
157
|
+
headers: {
|
|
158
|
+
Authorization: `Bearer ${token}`,
|
|
159
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
const message = await getDescriptiveErrorMessage(response)
|
|
165
|
+
throw new Error(`Token could not be validated. ${message}`)
|
|
166
|
+
}
|
|
167
|
+
return token
|
|
168
|
+
},
|
|
169
|
+
}))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export default stateModelFactory
|
|
173
|
+
export type GoogleDriveOAuthStateModel = ReturnType<typeof stateModelFactory>
|
|
174
|
+
export type GoogleDriveOAuthModel = Instance<GoogleDriveOAuthStateModel>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogTitle,
|
|
7
|
+
DialogActions,
|
|
8
|
+
TextField,
|
|
9
|
+
} from '@mui/material'
|
|
10
|
+
|
|
11
|
+
export const HTTPBasicLoginForm = ({
|
|
12
|
+
internetAccountId,
|
|
13
|
+
handleClose,
|
|
14
|
+
}: {
|
|
15
|
+
internetAccountId: string
|
|
16
|
+
handleClose: (arg?: string) => void
|
|
17
|
+
}) => {
|
|
18
|
+
const [username, setUsername] = useState('')
|
|
19
|
+
const [password, setPassword] = useState('')
|
|
20
|
+
|
|
21
|
+
function onSubmit(event: React.FormEvent<HTMLFormElement>) {
|
|
22
|
+
if (username && password) {
|
|
23
|
+
handleClose(btoa(`${username}:${password}`))
|
|
24
|
+
} else {
|
|
25
|
+
handleClose()
|
|
26
|
+
}
|
|
27
|
+
event.preventDefault()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<Dialog open maxWidth="xl" data-testid="login-httpbasic">
|
|
33
|
+
<DialogTitle>Log In for {internetAccountId}</DialogTitle>
|
|
34
|
+
<form onSubmit={onSubmit}>
|
|
35
|
+
<DialogContent style={{ display: 'flex', flexDirection: 'column' }}>
|
|
36
|
+
<TextField
|
|
37
|
+
required
|
|
38
|
+
label="Username"
|
|
39
|
+
variant="outlined"
|
|
40
|
+
inputProps={{ 'data-testid': 'login-httpbasic-username' }}
|
|
41
|
+
onChange={event => setUsername(event.target.value)}
|
|
42
|
+
margin="dense"
|
|
43
|
+
/>
|
|
44
|
+
<TextField
|
|
45
|
+
required
|
|
46
|
+
label="Password"
|
|
47
|
+
type="password"
|
|
48
|
+
autoComplete="current-password"
|
|
49
|
+
variant="outlined"
|
|
50
|
+
inputProps={{ 'data-testid': 'login-httpbasic-password' }}
|
|
51
|
+
onChange={event => setPassword(event.target.value)}
|
|
52
|
+
margin="dense"
|
|
53
|
+
/>
|
|
54
|
+
</DialogContent>
|
|
55
|
+
<DialogActions>
|
|
56
|
+
<Button variant="contained" color="primary" type="submit">
|
|
57
|
+
Submit
|
|
58
|
+
</Button>
|
|
59
|
+
<Button
|
|
60
|
+
variant="contained"
|
|
61
|
+
type="submit"
|
|
62
|
+
onClick={() => handleClose()}
|
|
63
|
+
>
|
|
64
|
+
Cancel
|
|
65
|
+
</Button>
|
|
66
|
+
</DialogActions>
|
|
67
|
+
</form>
|
|
68
|
+
</Dialog>
|
|
69
|
+
</>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ConfigurationSchema } from '@jbrowse/core/configuration'
|
|
2
|
+
import { Instance } from 'mobx-state-tree'
|
|
3
|
+
import { BaseInternetAccountConfig } from '@jbrowse/core/pluggableElementTypes/models'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* #config HTTPBasicInternetAccount
|
|
7
|
+
*/
|
|
8
|
+
function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
9
|
+
|
|
10
|
+
const HTTPBasicConfigSchema = ConfigurationSchema(
|
|
11
|
+
'HTTPBasicInternetAccount',
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* #slot
|
|
15
|
+
*/
|
|
16
|
+
tokenType: {
|
|
17
|
+
description: 'a custom name for a token to include in the header',
|
|
18
|
+
type: 'string',
|
|
19
|
+
defaultValue: 'Basic',
|
|
20
|
+
},
|
|
21
|
+
/**
|
|
22
|
+
* #slot
|
|
23
|
+
*/
|
|
24
|
+
validateWithHEAD: {
|
|
25
|
+
description: 'validate the token with a HEAD request before using it',
|
|
26
|
+
type: 'boolean',
|
|
27
|
+
defaultValue: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
/**
|
|
32
|
+
* #baseConfiguration
|
|
33
|
+
*/
|
|
34
|
+
baseConfiguration: BaseInternetAccountConfig,
|
|
35
|
+
explicitlyTyped: true,
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
export type HTTPBasicInternetAccountConfigModel = typeof HTTPBasicConfigSchema
|
|
40
|
+
|
|
41
|
+
export type HTTPBasicInternetAccountConfig =
|
|
42
|
+
Instance<HTTPBasicInternetAccountConfigModel>
|
|
43
|
+
export default HTTPBasicConfigSchema
|