@openneuro/server 4.19.3 → 4.20.0-alpha.1
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/Dockerfile +1 -1
- package/package.json +23 -26
- package/src/app.ts +119 -0
- package/src/datalad/files.ts +1 -1
- package/src/graphql/__tests__/__snapshots__/permissions.spec.js.snap +1 -1
- package/src/graphql/__tests__/permissions.spec.js +1 -1
- package/src/graphql/permissions.js +7 -9
- package/src/graphql/pubsub.js +4 -9
- package/src/graphql/resolvers/dataset.js +3 -3
- package/src/graphql/resolvers/draft.js +1 -1
- package/src/graphql/resolvers/git.ts +1 -1
- package/src/graphql/resolvers/{metadata.js → metadata.ts} +26 -4
- package/src/graphql/resolvers/mutation.js +3 -3
- package/src/graphql/resolvers/{permissions.js → permissions.ts} +16 -8
- package/src/graphql/resolvers/query.js +2 -0
- package/src/graphql/resolvers/reviewer.ts +1 -1
- package/src/graphql/resolvers/snapshots.js +1 -1
- package/src/graphql/resolvers/{summary.js → summary.ts} +2 -2
- package/src/graphql/resolvers/upload.js +1 -1
- package/src/graphql/resolvers/user.js +15 -1
- package/src/graphql/schema.js +3 -1
- package/src/graphql/utils/file.js +1 -1
- package/src/handlers/datalad.js +13 -16
- package/src/handlers/doi.js +1 -1
- package/src/libs/authentication/__tests__/jwt.spec.js +1 -1
- package/src/libs/authentication/{jwt.js → jwt.ts} +25 -7
- package/src/libs/authentication/orcid.js +1 -1
- package/src/libs/authentication/passport.js +1 -1
- package/src/libs/doi/__tests__/__snapshots__/doi.spec.js.snap +1 -1
- package/src/libs/email/templates/__tests__/__snapshots__/comment-created.spec.ts.snap +1 -1
- package/src/libs/email/templates/__tests__/__snapshots__/dataset-deleted.spec.ts.snap +1 -1
- package/src/libs/email/templates/__tests__/__snapshots__/owner-unsubscribed.spec.ts.snap +1 -1
- package/src/libs/email/templates/__tests__/__snapshots__/snapshot-created.spec.ts.snap +1 -1
- package/src/libs/email/templates/__tests__/__snapshots__/snapshot-reminder.spec.ts.snap +1 -1
- package/src/libs/subscription-server.js +2 -1
- package/src/models/summary.ts +2 -0
- package/src/routes.js +2 -2
- package/src/{server.js → server.ts} +5 -7
- package/src/app.js +0 -94
package/Dockerfile
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openneuro/server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.20.0-alpha.1",
|
|
4
4
|
"description": "Core service for the OpenNeuro platform.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -16,35 +16,37 @@
|
|
|
16
16
|
"author": "Squishymedia",
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@apollo/client": "3.7.2",
|
|
19
|
+
"@apollo/server": "4.9.3",
|
|
20
|
+
"@apollo/utils.keyvadapter": "3.0.0",
|
|
19
21
|
"@elastic/elasticsearch": "7.15.0",
|
|
20
|
-
"@
|
|
22
|
+
"@graphql-tools/schema": "^10.0.0",
|
|
23
|
+
"@keyv/redis": "^2.7.0",
|
|
24
|
+
"@openneuro/search": "^4.20.0-alpha.1",
|
|
21
25
|
"@passport-next/passport-google-oauth2": "^1.0.0",
|
|
22
26
|
"@sentry/node": "^4.5.3",
|
|
23
|
-
"apollo-server": "2.25.4",
|
|
24
|
-
"apollo-server-cache-redis": "1.4.0",
|
|
25
|
-
"apollo-server-express": "2.25.3",
|
|
26
27
|
"base64url": "^3.0.0",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
28
|
+
"cookie-parser": "^1.4.6",
|
|
29
|
+
"cors": "^2.8.5",
|
|
29
30
|
"date-fns": "^2.16.1",
|
|
30
31
|
"draft-js": "^0.11.7",
|
|
31
32
|
"draft-js-export-html": "^1.4.1",
|
|
32
|
-
"elastic-apm-node": "3.
|
|
33
|
-
"express": "
|
|
34
|
-
"graphql": "
|
|
33
|
+
"elastic-apm-node": "3.49.1",
|
|
34
|
+
"express": "4.18.2",
|
|
35
|
+
"graphql": "16.6.0",
|
|
35
36
|
"graphql-bigint": "^1.0.0",
|
|
36
|
-
"graphql-compose": "
|
|
37
|
+
"graphql-compose": "9.0.10",
|
|
37
38
|
"graphql-iso-date": "^3.6.1",
|
|
38
39
|
"graphql-redis-subscriptions": "2.1.0",
|
|
39
40
|
"graphql-subscriptions": "^1.1.0",
|
|
40
|
-
"graphql-tools": "
|
|
41
|
+
"graphql-tools": "9.0.0",
|
|
41
42
|
"immutable": "^3.8.2",
|
|
42
43
|
"ioredis": "4.17.3",
|
|
43
44
|
"jsdom": "^11.6.2",
|
|
44
45
|
"jsonwebtoken": "^9.0.0",
|
|
46
|
+
"keyv": "^4.5.3",
|
|
45
47
|
"mime-types": "^2.1.19",
|
|
46
48
|
"moment": "^2.14.1",
|
|
47
|
-
"mongoose": "^
|
|
49
|
+
"mongoose": "^6.11.3",
|
|
48
50
|
"morgan": "^1.6.1",
|
|
49
51
|
"node-mailjet": "^3.3.5",
|
|
50
52
|
"object-hash": "2.1.1",
|
|
@@ -59,38 +61,33 @@
|
|
|
59
61
|
"request": "^2.83.0",
|
|
60
62
|
"semver": "^5.5.0",
|
|
61
63
|
"sitemap": "^2.1.0",
|
|
62
|
-
"subscriptions-transport-ws": "0.9.18",
|
|
63
64
|
"superagent": "^3.8.2",
|
|
64
65
|
"ts-node": "9.1.1",
|
|
65
|
-
"typescript": "
|
|
66
|
+
"typescript": "5.1.6",
|
|
66
67
|
"underscore": "^1.8.3",
|
|
67
68
|
"uuid": "^3.0.1",
|
|
68
69
|
"xmldoc": "^1.1.0"
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
71
|
-
"@
|
|
72
|
-
"@babel/plugin-proposal-object-rest-spread": "^7.6.2",
|
|
73
|
-
"@babel/plugin-proposal-optional-chaining": "^7.6.0",
|
|
74
|
-
"@babel/plugin-syntax-object-rest-spread": "^7.0.0",
|
|
75
|
-
"@babel/preset-env": "^7.6.3",
|
|
76
|
-
"@babel/runtime-corejs3": "^7.13.10",
|
|
72
|
+
"@types/cors": "^2",
|
|
77
73
|
"@types/draft-js": "^0.10.43",
|
|
74
|
+
"@types/express": "^4.17.17",
|
|
75
|
+
"@types/express-serve-static-core": "^4.17.35",
|
|
78
76
|
"@types/ioredis": "^4.17.1",
|
|
77
|
+
"@types/ioredis-mock": "^8.2.2",
|
|
79
78
|
"@types/node-mailjet": "^3",
|
|
80
79
|
"@types/semver": "^5",
|
|
81
|
-
"apollo-link-schema": "^1.2.5",
|
|
82
|
-
"babel-eslint": "^10.1.0",
|
|
83
80
|
"core-js": "^3.10.1",
|
|
84
|
-
"ioredis-mock": "^8.
|
|
81
|
+
"ioredis-mock": "^8.8.1",
|
|
85
82
|
"nodemon": "^2.0.7",
|
|
86
83
|
"supertest": "^3.0.0",
|
|
87
84
|
"ts-node-dev": "1.1.6",
|
|
88
85
|
"tsc-watch": "^4.2.9",
|
|
89
|
-
"vitest": "
|
|
86
|
+
"vitest": "0.33.0",
|
|
90
87
|
"vitest-fetch-mock": "^0.2.1"
|
|
91
88
|
},
|
|
92
89
|
"publishConfig": {
|
|
93
90
|
"access": "public"
|
|
94
91
|
},
|
|
95
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "60ca7376fddf72b9516b582ee46c585ef526a39f"
|
|
96
93
|
}
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/*eslint no-console: ["error", { allow: ["log"] }] */
|
|
2
|
+
/* eslint-disable no-unused-vars*/
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Express app setup
|
|
6
|
+
*/
|
|
7
|
+
import { createServer } from 'http'
|
|
8
|
+
import cors from 'cors'
|
|
9
|
+
import express, { urlencoded, json } from 'express'
|
|
10
|
+
import passport from 'passport'
|
|
11
|
+
import config from './config'
|
|
12
|
+
import routes from './routes'
|
|
13
|
+
import morgan from 'morgan'
|
|
14
|
+
import schema from './graphql/schema'
|
|
15
|
+
import { ApolloServer } from '@apollo/server'
|
|
16
|
+
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default'
|
|
17
|
+
import { expressMiddleware } from '@apollo/server/express4'
|
|
18
|
+
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'
|
|
19
|
+
import { KeyvAdapter } from '@apollo/utils.keyvadapter'
|
|
20
|
+
import Keyv from 'keyv'
|
|
21
|
+
import KeyvRedis from '@keyv/redis'
|
|
22
|
+
import cookieParser from 'cookie-parser'
|
|
23
|
+
import * as jwt from './libs/authentication/jwt'
|
|
24
|
+
import * as auth from './libs/authentication/states.js'
|
|
25
|
+
import { sitemapHandler } from './handlers/sitemap.js'
|
|
26
|
+
import { setupPassportAuth } from './libs/authentication/passport.js'
|
|
27
|
+
import { redis } from './libs/redis'
|
|
28
|
+
import { version } from './lerna.json'
|
|
29
|
+
export { Express } from 'express-serve-static-core'
|
|
30
|
+
|
|
31
|
+
interface OpenNeuroRequestContext {
|
|
32
|
+
user: string
|
|
33
|
+
isSuperUser: boolean
|
|
34
|
+
userInfo: {
|
|
35
|
+
id: string
|
|
36
|
+
exp: string
|
|
37
|
+
scopes: string[]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function expressApolloSetup() {
|
|
42
|
+
const app = express()
|
|
43
|
+
|
|
44
|
+
setupPassportAuth()
|
|
45
|
+
|
|
46
|
+
app.use(passport.initialize())
|
|
47
|
+
|
|
48
|
+
app.use((req, res, next) => {
|
|
49
|
+
res.set(config.headers)
|
|
50
|
+
res.type('application/json')
|
|
51
|
+
next()
|
|
52
|
+
})
|
|
53
|
+
app.use(morgan('short'))
|
|
54
|
+
app.use(cookieParser())
|
|
55
|
+
app.use(urlencoded({ extended: false, limit: '50mb' }))
|
|
56
|
+
app.use(json({ limit: '50mb' }))
|
|
57
|
+
|
|
58
|
+
// routing ---------------------------------------------------------
|
|
59
|
+
app.use('/sitemap.xml', sitemapHandler)
|
|
60
|
+
app.use(config.apiPrefix, routes)
|
|
61
|
+
|
|
62
|
+
const httpServer = createServer(app)
|
|
63
|
+
|
|
64
|
+
// Apollo server setup
|
|
65
|
+
const apolloServer = new ApolloServer<OpenNeuroRequestContext>({
|
|
66
|
+
schema,
|
|
67
|
+
// Always allow introspection - our schema is public
|
|
68
|
+
introspection: true,
|
|
69
|
+
cache: new KeyvAdapter(new Keyv({ store: new KeyvRedis(redis) })),
|
|
70
|
+
plugins: [
|
|
71
|
+
ApolloServerPluginLandingPageLocalDefault(),
|
|
72
|
+
ApolloServerPluginDrainHttpServer({
|
|
73
|
+
httpServer,
|
|
74
|
+
}),
|
|
75
|
+
{
|
|
76
|
+
async requestDidStart() {
|
|
77
|
+
return {
|
|
78
|
+
async willSendResponse(requestContext) {
|
|
79
|
+
const { response } = requestContext
|
|
80
|
+
if (
|
|
81
|
+
response.body.kind === 'single' &&
|
|
82
|
+
'data' in response.body.singleResult
|
|
83
|
+
) {
|
|
84
|
+
response.body.singleResult.extensions = {
|
|
85
|
+
...response.body.singleResult.extensions,
|
|
86
|
+
|
|
87
|
+
openneuro: { version },
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
await apolloServer.start()
|
|
98
|
+
|
|
99
|
+
// Setup GraphQL middleware
|
|
100
|
+
app.use(
|
|
101
|
+
['/graphql', '/crn/graphql'],
|
|
102
|
+
cors<cors.CorsRequest>(),
|
|
103
|
+
jwt.authenticate,
|
|
104
|
+
auth.optional,
|
|
105
|
+
expressMiddleware(apolloServer, {
|
|
106
|
+
context: async ({ req }) => {
|
|
107
|
+
if (req.isAuthenticated()) {
|
|
108
|
+
return {
|
|
109
|
+
user: req.user.id,
|
|
110
|
+
isSuperUser: req.user.admin,
|
|
111
|
+
userInfo: req.user,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return app
|
|
119
|
+
}
|
package/src/datalad/files.ts
CHANGED
|
@@ -12,7 +12,7 @@ export const encodeFilePath = (path: string): string => {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Convert
|
|
15
|
+
* Convert from URL compatible path to filepath
|
|
16
16
|
* @param {String} path
|
|
17
17
|
*/
|
|
18
18
|
export const decodeFilePath = (path: string): string => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import config from '../config.js'
|
|
2
|
-
import {
|
|
2
|
+
import { GraphQLError } from 'graphql'
|
|
3
3
|
import Permission from '../models/permission'
|
|
4
4
|
import Dataset from '../models/dataset'
|
|
5
5
|
import Deletion from '../models/deletion'
|
|
@@ -52,9 +52,9 @@ export const checkPermissionLevel = (permission, state) => {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export class DeletedDatasetError extends
|
|
55
|
+
export class DeletedDatasetError extends GraphQLError {
|
|
56
56
|
constructor(datasetId, reason, redirect = undefined) {
|
|
57
|
-
let
|
|
57
|
+
let extensions
|
|
58
58
|
if (redirect) {
|
|
59
59
|
try {
|
|
60
60
|
// Validate URL before we attach it to the API response
|
|
@@ -65,17 +65,15 @@ export class DeletedDatasetError extends ApolloError {
|
|
|
65
65
|
url.pathname.startsWith('/datasets')
|
|
66
66
|
) {
|
|
67
67
|
// Only return a relative path to avoid cross site risks
|
|
68
|
-
|
|
68
|
+
extensions = { code: 'DELETED_DATASET', redirect: url.pathname }
|
|
69
69
|
}
|
|
70
70
|
} catch (err) {
|
|
71
71
|
// Do nothing
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
super(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
extension,
|
|
78
|
-
)
|
|
74
|
+
super(`Dataset ${datasetId} has been deleted. Reason: ${reason}.`, {
|
|
75
|
+
extensions,
|
|
76
|
+
})
|
|
79
77
|
}
|
|
80
78
|
}
|
|
81
79
|
|
package/src/graphql/pubsub.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
async function* asyncIterator(_) {
|
|
2
|
+
yield null
|
|
3
|
+
}
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
publisher: new Redis(config.redis),
|
|
7
|
-
subscriber: new Redis(config.redis),
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
export default pubsub
|
|
5
|
+
export default { publish: (_, __) => {}, asyncIterator }
|
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
checkDatasetRead,
|
|
8
8
|
checkDatasetWrite,
|
|
9
9
|
checkDatasetAdmin,
|
|
10
|
-
} from '../permissions
|
|
10
|
+
} from '../permissions'
|
|
11
11
|
import { user } from './user.js'
|
|
12
|
-
import { permissions } from './permissions
|
|
12
|
+
import { permissions } from './permissions'
|
|
13
13
|
import { datasetComments } from './comment.js'
|
|
14
|
-
import { metadata } from './metadata
|
|
14
|
+
import { metadata } from './metadata'
|
|
15
15
|
import { history } from './history.js'
|
|
16
16
|
import * as dataladAnalytics from '../../datalad/analytics.js'
|
|
17
17
|
import DatasetModel from '../../models/dataset'
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import Snapshot from '../../models/snapshot'
|
|
2
|
-
import
|
|
2
|
+
import { LeanDocument } from 'mongoose'
|
|
3
|
+
import DatasetModel from '../../models/dataset'
|
|
4
|
+
import MetadataModel, { MetadataDocument } from '../../models/metadata'
|
|
3
5
|
import { latestSnapshot } from './snapshots'
|
|
4
6
|
import { permissions } from './permissions'
|
|
5
7
|
|
|
@@ -8,7 +10,11 @@ import { permissions } from './permissions'
|
|
|
8
10
|
*
|
|
9
11
|
* User modified fields are queried from the Metadata model and dynamic metadata is updated from the latest snapshot
|
|
10
12
|
*/
|
|
11
|
-
export const metadata = async (
|
|
13
|
+
export const metadata = async (
|
|
14
|
+
dataset,
|
|
15
|
+
_,
|
|
16
|
+
context,
|
|
17
|
+
): Promise<LeanDocument<MetadataDocument>> => {
|
|
12
18
|
const record = await MetadataModel.findOne({
|
|
13
19
|
datasetId: dataset.id,
|
|
14
20
|
}).lean()
|
|
@@ -24,7 +30,7 @@ export const metadata = async (dataset, _, context) => {
|
|
|
24
30
|
for (const user of userPermissions) {
|
|
25
31
|
if (user.level === 'admin') {
|
|
26
32
|
const userObj = await user.user
|
|
27
|
-
adminUsers.push(userObj.
|
|
33
|
+
adminUsers.push(userObj.name)
|
|
28
34
|
}
|
|
29
35
|
}
|
|
30
36
|
const firstSnapshot = await Snapshot.find({ datasetId: dataset.id }).sort({
|
|
@@ -44,7 +50,7 @@ export const metadata = async (dataset, _, context) => {
|
|
|
44
50
|
adminUsers,
|
|
45
51
|
firstSnapshotCreatedAt,
|
|
46
52
|
latestSnapshotCreatedAt: snapshot.created,
|
|
47
|
-
|
|
53
|
+
ages: summary?.subjectMetadata?.map(s => s.age as number),
|
|
48
54
|
modalities: summary?.modalities || [],
|
|
49
55
|
dataProcessed: summary?.dataProcessed || null,
|
|
50
56
|
}
|
|
@@ -60,3 +66,19 @@ export const addMetadata = async (obj, { datasetId, metadata }) => {
|
|
|
60
66
|
})
|
|
61
67
|
return result
|
|
62
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve all public datasets and return metadata
|
|
72
|
+
*/
|
|
73
|
+
export async function publicMetadata(
|
|
74
|
+
obj,
|
|
75
|
+
): Promise<Array<LeanDocument<MetadataDocument>>> {
|
|
76
|
+
const datasets = await DatasetModel.find({
|
|
77
|
+
public: true,
|
|
78
|
+
}).lean()
|
|
79
|
+
const dsMetadata: LeanDocument<MetadataDocument>[] = []
|
|
80
|
+
for (const ds of datasets) {
|
|
81
|
+
dsMetadata.push(await metadata(ds, null, {}))
|
|
82
|
+
}
|
|
83
|
+
return dsMetadata
|
|
84
|
+
}
|
|
@@ -18,9 +18,9 @@ import {
|
|
|
18
18
|
undoDeprecateSnapshot,
|
|
19
19
|
} from './snapshots.js'
|
|
20
20
|
import { removeUser, setAdmin, setBlocked } from './user.js'
|
|
21
|
-
import { updateSummary } from './summary
|
|
21
|
+
import { updateSummary } from './summary'
|
|
22
22
|
import { revalidate, updateValidation } from './validation.js'
|
|
23
|
-
import { updatePermissions, removePermissions } from './permissions
|
|
23
|
+
import { updatePermissions, removePermissions } from './permissions'
|
|
24
24
|
import { followDataset } from './follow.js'
|
|
25
25
|
import { starDataset } from './stars.js'
|
|
26
26
|
import { publishDataset } from './publish.js'
|
|
@@ -28,7 +28,7 @@ import { updateDescription, updateDescriptionList } from './description.js'
|
|
|
28
28
|
import { updateReadme } from './readme.js'
|
|
29
29
|
import { addComment, editComment, deleteComment } from './comment.js'
|
|
30
30
|
import { subscribeToNewsletter } from './newsletter'
|
|
31
|
-
import { addMetadata } from './metadata
|
|
31
|
+
import { addMetadata } from './metadata'
|
|
32
32
|
import { prepareUpload, finishUpload } from './upload.js'
|
|
33
33
|
import { prepareRepoAccess } from './git'
|
|
34
34
|
import { cacheClear } from './cache'
|
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
import User from '../../models/user'
|
|
2
|
-
import Permission from '../../models/permission'
|
|
1
|
+
import User, { UserDocument } from '../../models/user'
|
|
2
|
+
import Permission, { PermissionDocument } from '../../models/permission'
|
|
3
3
|
import { checkDatasetAdmin } from '../permissions'
|
|
4
4
|
import { user } from './user'
|
|
5
5
|
import pubsub from '../pubsub.js'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
interface DatasetPermission {
|
|
8
|
+
id: string
|
|
9
|
+
userPermissions: (PermissionDocument & { user: Promise<UserDocument> })[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function permissions(ds): Promise<DatasetPermission> {
|
|
8
13
|
const permissions = await Permission.find({ datasetId: ds.id }).exec()
|
|
9
14
|
return {
|
|
10
15
|
id: ds.id,
|
|
11
|
-
userPermissions: permissions.map(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
userPermissions: permissions.map(
|
|
17
|
+
userPermission =>
|
|
18
|
+
({
|
|
19
|
+
...userPermission.toJSON(),
|
|
20
|
+
user: user(ds, { id: userPermission.userId }),
|
|
21
|
+
} as PermissionDocument & { user: Promise<UserDocument> }),
|
|
22
|
+
),
|
|
15
23
|
}
|
|
16
24
|
}
|
|
17
25
|
|
|
@@ -53,7 +61,7 @@ export const updatePermissions = async (obj, args, { user, userInfo }) => {
|
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
const userPromises = users.map(user => {
|
|
56
|
-
return new Promise((resolve, reject) => {
|
|
64
|
+
return new Promise<void>((resolve, reject) => {
|
|
57
65
|
Permission.updateOne(
|
|
58
66
|
{
|
|
59
67
|
datasetId: args.datasetId,
|
|
@@ -5,6 +5,7 @@ import { dataset, datasets } from './dataset.js'
|
|
|
5
5
|
import { snapshot, participantCount } from './snapshots.js'
|
|
6
6
|
import { user, users } from './user.js'
|
|
7
7
|
import { flaggedFiles } from './flaggedFiles'
|
|
8
|
+
import { publicMetadata } from './metadata'
|
|
8
9
|
|
|
9
10
|
const Query = {
|
|
10
11
|
dataset,
|
|
@@ -14,6 +15,7 @@ const Query = {
|
|
|
14
15
|
snapshot,
|
|
15
16
|
participantCount,
|
|
16
17
|
flaggedFiles,
|
|
18
|
+
publicMetadata,
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export default Query
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import config from '../../config'
|
|
2
2
|
import Reviewer from '../../models/reviewer'
|
|
3
3
|
import { checkDatasetAdmin } from '../permissions.js'
|
|
4
|
-
import { generateReviewerToken } from '../../libs/authentication/jwt
|
|
4
|
+
import { generateReviewerToken } from '../../libs/authentication/jwt'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Create an anonymous read-only access key
|
|
@@ -4,7 +4,7 @@ import { onBrainlife } from './brainlife'
|
|
|
4
4
|
import { checkDatasetRead, checkDatasetWrite } from '../permissions.js'
|
|
5
5
|
import { readme } from './readme.js'
|
|
6
6
|
import { description } from './description.js'
|
|
7
|
-
import { summary } from './summary
|
|
7
|
+
import { summary } from './summary'
|
|
8
8
|
import { snapshotIssues } from './issues.js'
|
|
9
9
|
import { getFiles } from '../../datalad/files'
|
|
10
10
|
import Summary from '../../models/summary'
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import Summary from '../../models/summary'
|
|
1
|
+
import Summary, { SummaryDocument } from '../../models/summary'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Summary resolver
|
|
5
5
|
*/
|
|
6
|
-
export
|
|
6
|
+
export async function summary(dataset): Promise<Partial<SummaryDocument>> {
|
|
7
7
|
const datasetSummary = (
|
|
8
8
|
await Summary.findOne({
|
|
9
9
|
id: dataset.revision,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Upload from '../../models/upload'
|
|
2
2
|
import { checkDatasetWrite } from '../permissions.js'
|
|
3
|
-
import { generateUploadToken } from '../../libs/authentication/jwt
|
|
3
|
+
import { generateUploadToken } from '../../libs/authentication/jwt'
|
|
4
4
|
import { finishUploadRequest } from '../../datalad/upload.js'
|
|
5
5
|
import { getDatasetEndpoint } from '../../libs/datalad-service.js'
|
|
6
6
|
|
|
@@ -45,4 +45,18 @@ export const setBlocked = (obj, { id, blocked }, { userInfo }) => {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
const UserResolvers = {
|
|
49
|
+
id: obj => obj.id,
|
|
50
|
+
provider: obj => obj.provider,
|
|
51
|
+
avatar: obj => obj.avatar,
|
|
52
|
+
orcid: obj => obj.orcid,
|
|
53
|
+
created: obj => obj.created,
|
|
54
|
+
modified: obj => obj.modified,
|
|
55
|
+
lastSeen: obj => obj.lastSeen,
|
|
56
|
+
email: obj => obj.email,
|
|
57
|
+
name: obj => obj.name,
|
|
58
|
+
admin: obj => obj.admin,
|
|
59
|
+
blocked: obj => obj.blocked,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default UserResolvers
|
package/src/graphql/schema.js
CHANGED
|
@@ -97,6 +97,8 @@ export const typeDefs = `
|
|
|
97
97
|
"Get files that have already been deleted, default false."
|
|
98
98
|
deleted: Boolean = false
|
|
99
99
|
): [FlaggedFile]
|
|
100
|
+
# All public dataset metadata
|
|
101
|
+
publicMetadata: [Metadata] @cacheControl(maxAge: 86400, scope: PUBLIC)
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
type Mutation {
|
|
@@ -527,7 +529,7 @@ export const typeDefs = `
|
|
|
527
529
|
id: ID!
|
|
528
530
|
# ID of user who flagged snapshot as deprecated
|
|
529
531
|
user: String
|
|
530
|
-
# Reason for deprecating
|
|
532
|
+
# Reason for deprecating snapshot
|
|
531
533
|
reason: String
|
|
532
534
|
# Timestamp of snapshot deprecation
|
|
533
535
|
timestamp: Date
|
|
@@ -5,7 +5,7 @@ export const filterRemovedAnnexObjects =
|
|
|
5
5
|
const removedAnnexObjectKeys = (
|
|
6
6
|
await BadAnnexObject.find({ datasetId }).exec()
|
|
7
7
|
).map(({ annexKey }) => annexKey)
|
|
8
|
-
// keep files that
|
|
8
|
+
// keep files that haven't had their annex objects removed
|
|
9
9
|
return userInfo?.admin
|
|
10
10
|
? files
|
|
11
11
|
: files.filter(({ key }) => !removedAnnexObjectKeys.includes(key))
|
package/src/handlers/datalad.js
CHANGED
|
@@ -40,23 +40,20 @@ export const getFile = async (req, res) => {
|
|
|
40
40
|
const uri = snapshotId
|
|
41
41
|
? `http://${worker}/datasets/${datasetId}/snapshots/${snapshotId}/files/${filename}`
|
|
42
42
|
: `http://${worker}/datasets/${datasetId}/files/${filename}`
|
|
43
|
-
return (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
)
|
|
51
|
-
return r.body
|
|
52
|
-
})
|
|
43
|
+
return fetch(uri)
|
|
44
|
+
.then(r => {
|
|
45
|
+
// Set the content length (allow clients to catch HTTP issues better)
|
|
46
|
+
res.setHeader('Content-Length', Number(r.headers.get('content-length')))
|
|
47
|
+
return r.body
|
|
48
|
+
})
|
|
49
|
+
.then(stream =>
|
|
53
50
|
// @ts-expect-error
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
Readable.fromWeb(stream, { highWaterMark: 4194304 }).pipe(res),
|
|
52
|
+
)
|
|
53
|
+
.catch(err => {
|
|
54
|
+
console.error(err)
|
|
55
|
+
res.status(500).send('Internal error transferring requested file')
|
|
56
|
+
})
|
|
60
57
|
}
|
|
61
58
|
}
|
|
62
59
|
|
package/src/handlers/doi.js
CHANGED
|
@@ -42,7 +42,7 @@ export async function createSnapshotDoi(req, res) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// Have
|
|
45
|
+
// Have separate function to get Doi that does not require any authorization
|
|
46
46
|
export async function getDoi(req, res) {
|
|
47
47
|
const datasetId = req.params.datasetId
|
|
48
48
|
const snapshotId = req.params.snapshotId
|
|
@@ -5,8 +5,26 @@ import { decrypt } from './crypto'
|
|
|
5
5
|
import User from '../../models/user'
|
|
6
6
|
import config from '../../config.js'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
interface OpenNeuroTokenProfile {
|
|
9
|
+
sub: string
|
|
10
|
+
email: string
|
|
11
|
+
provider: string
|
|
12
|
+
name: string
|
|
13
|
+
admin: boolean
|
|
14
|
+
iat: number
|
|
15
|
+
exp: number
|
|
16
|
+
// Tokens may be scoped and limited to one dataset
|
|
17
|
+
scopes?: string[]
|
|
18
|
+
dataset?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const buildToken = (
|
|
22
|
+
config,
|
|
23
|
+
user,
|
|
24
|
+
expiresIn,
|
|
25
|
+
options?: { scopes?: string[]; dataset?: string },
|
|
26
|
+
): string => {
|
|
27
|
+
const fields: Omit<OpenNeuroTokenProfile, 'iat' | 'exp'> = {
|
|
10
28
|
sub: user.id,
|
|
11
29
|
email: user.email,
|
|
12
30
|
provider: user.provider,
|
|
@@ -15,7 +33,7 @@ export const buildToken = (config, user, expiresIn, options) => {
|
|
|
15
33
|
}
|
|
16
34
|
// Allow extensions of the base token format
|
|
17
35
|
if (options) {
|
|
18
|
-
if ('scopes' in options) {
|
|
36
|
+
if (options && 'scopes' in options) {
|
|
19
37
|
fields.scopes = options.scopes
|
|
20
38
|
}
|
|
21
39
|
if ('dataset' in options) {
|
|
@@ -24,7 +42,7 @@ export const buildToken = (config, user, expiresIn, options) => {
|
|
|
24
42
|
}
|
|
25
43
|
return jwt.sign(fields, config.auth.jwt.secret, {
|
|
26
44
|
expiresIn,
|
|
27
|
-
})
|
|
45
|
+
}) as string
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
// Helper to generate a JWT containing user info
|
|
@@ -77,7 +95,7 @@ export function generateReviewerToken(
|
|
|
77
95
|
/**
|
|
78
96
|
* Generate an git repo token
|
|
79
97
|
*
|
|
80
|
-
*
|
|
98
|
+
* Similarly to the upload token, this shorter lived token is specific to git access
|
|
81
99
|
*/
|
|
82
100
|
export function generateRepoToken(user, datasetId, expiresIn = 60 * 60 * 24) {
|
|
83
101
|
const options = {
|
|
@@ -111,8 +129,8 @@ export const jwtFromRequest = req => {
|
|
|
111
129
|
}
|
|
112
130
|
}
|
|
113
131
|
|
|
114
|
-
export const decodeJWT = token => {
|
|
115
|
-
return jwt.decode(token)
|
|
132
|
+
export const decodeJWT = (token: string): OpenNeuroTokenProfile => {
|
|
133
|
+
return jwt.decode(token) as OpenNeuroTokenProfile
|
|
116
134
|
}
|
|
117
135
|
|
|
118
136
|
export const parsedJwtFromRequest = req => {
|
|
@@ -6,7 +6,7 @@ import { Strategy as ORCIDStrategy } from 'passport-orcid'
|
|
|
6
6
|
import config from '../../config.js'
|
|
7
7
|
import User from '../../models/user'
|
|
8
8
|
import { encrypt } from './crypto'
|
|
9
|
-
import { addJWT, jwtFromRequest
|
|
9
|
+
import { addJWT, jwtFromRequest } from './jwt'
|
|
10
10
|
import orcid from '../orcid.js'
|
|
11
11
|
|
|
12
12
|
const PROVIDERS = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execute, subscribe } from 'graphql'
|
|
1
|
+
/*import { execute, subscribe } from 'graphql'
|
|
2
2
|
import { SubscriptionServer } from 'subscriptions-transport-ws'
|
|
3
3
|
import schema from '../graphql/schema.js'
|
|
4
4
|
|
|
@@ -17,3 +17,4 @@ const subscriptionServerFactory = httpserver =>
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
export default subscriptionServerFactory
|
|
20
|
+
*/
|
package/src/models/summary.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface SummaryDocument extends Document {
|
|
|
17
17
|
subjectMetadata: Record<string, any>
|
|
18
18
|
tasks: string[]
|
|
19
19
|
modalities: string[]
|
|
20
|
+
primaryModality: string
|
|
20
21
|
secondaryModalities: string[]
|
|
21
22
|
dataTypes: string[]
|
|
22
23
|
totalFiles: number
|
|
@@ -33,6 +34,7 @@ const summarySchema = new Schema({
|
|
|
33
34
|
subjectMetadata: Object,
|
|
34
35
|
tasks: [String],
|
|
35
36
|
modalities: [String],
|
|
37
|
+
primaryModality: String,
|
|
36
38
|
secondaryModalities: [String],
|
|
37
39
|
dataTypes: [String],
|
|
38
40
|
totalFiles: Number,
|
package/src/routes.js
CHANGED
|
@@ -9,7 +9,7 @@ import * as subscriptions from './handlers/subscriptions'
|
|
|
9
9
|
import verifyUser from './libs/authentication/verifyUser.js'
|
|
10
10
|
import * as google from './libs/authentication/google.js'
|
|
11
11
|
import * as orcid from './libs/authentication/orcid.js'
|
|
12
|
-
import * as jwt from './libs/authentication/jwt
|
|
12
|
+
import * as jwt from './libs/authentication/jwt'
|
|
13
13
|
import * as auth from './libs/authentication/states.js'
|
|
14
14
|
import * as doi from './handlers/doi'
|
|
15
15
|
import { sitemapHandler } from './handlers/sitemap.js'
|
|
@@ -148,7 +148,7 @@ const routes = [
|
|
|
148
148
|
{
|
|
149
149
|
method: 'get',
|
|
150
150
|
url: '/auth/orcid',
|
|
151
|
-
|
|
151
|
+
middleware: [noCache],
|
|
152
152
|
handler: orcid.requestAuth,
|
|
153
153
|
},
|
|
154
154
|
{
|
|
@@ -7,11 +7,9 @@ apm.start({
|
|
|
7
7
|
|
|
8
8
|
import { createServer } from 'http'
|
|
9
9
|
import mongoose from 'mongoose'
|
|
10
|
-
import subscriptionServerFactory from './libs/subscription-server.js'
|
|
11
10
|
import { connect as redisConnect } from './libs/redis'
|
|
12
11
|
import config from './config'
|
|
13
|
-
import
|
|
14
|
-
import { version } from './lerna.json'
|
|
12
|
+
import { expressApolloSetup } from './app'
|
|
15
13
|
|
|
16
14
|
const redisConnectionSetup = async () => {
|
|
17
15
|
try {
|
|
@@ -23,18 +21,18 @@ const redisConnectionSetup = async () => {
|
|
|
23
21
|
}
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
mongoose.connect(config.mongo.url, {
|
|
24
|
+
void mongoose.connect(config.mongo.url, {
|
|
27
25
|
dbName: config.mongo.dbName,
|
|
28
26
|
connectTimeoutMS: config.mongo.connectTimeoutMS,
|
|
29
27
|
})
|
|
30
28
|
|
|
31
|
-
redisConnectionSetup().then(() => {
|
|
32
|
-
const app =
|
|
29
|
+
void redisConnectionSetup().then(async () => {
|
|
30
|
+
const app = await expressApolloSetup()
|
|
33
31
|
const server = createServer(app)
|
|
34
32
|
server.listen(config.port, () => {
|
|
35
33
|
// eslint-disable-next-line no-console
|
|
36
34
|
console.log('Server is listening on port ' + config.port)
|
|
37
35
|
// Setup GraphQL subscription transport
|
|
38
|
-
subscriptionServerFactory(server)
|
|
36
|
+
//subscriptionServerFactory(server)
|
|
39
37
|
})
|
|
40
38
|
})
|
package/src/app.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/*eslint no-console: ["error", { allow: ["log"] }] */
|
|
2
|
-
/* eslint-disable no-unused-vars*/
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Express app setup
|
|
6
|
-
*/
|
|
7
|
-
import express from 'express'
|
|
8
|
-
import passport from 'passport'
|
|
9
|
-
import config from './config'
|
|
10
|
-
import routes from './routes'
|
|
11
|
-
import morgan from 'morgan'
|
|
12
|
-
import schema from './graphql/schema'
|
|
13
|
-
import { ApolloServer } from 'apollo-server-express'
|
|
14
|
-
import { BaseRedisCache } from 'apollo-server-cache-redis'
|
|
15
|
-
import bodyParser from 'body-parser'
|
|
16
|
-
import cookieParser from 'cookie-parser'
|
|
17
|
-
import * as jwt from './libs/authentication/jwt.js'
|
|
18
|
-
import * as auth from './libs/authentication/states.js'
|
|
19
|
-
import { sitemapHandler } from './handlers/sitemap.js'
|
|
20
|
-
import { setupPassportAuth } from './libs/authentication/passport.js'
|
|
21
|
-
import { redis } from './libs/redis'
|
|
22
|
-
import { version } from './lerna.json'
|
|
23
|
-
|
|
24
|
-
// test flag disables Sentry for tests
|
|
25
|
-
export default test => {
|
|
26
|
-
const app = express()
|
|
27
|
-
|
|
28
|
-
setupPassportAuth()
|
|
29
|
-
|
|
30
|
-
app.use(passport.initialize())
|
|
31
|
-
|
|
32
|
-
app.use((req, res, next) => {
|
|
33
|
-
res.set(config.headers)
|
|
34
|
-
res.type('application/json')
|
|
35
|
-
next()
|
|
36
|
-
})
|
|
37
|
-
app.use(morgan('short'))
|
|
38
|
-
app.use(cookieParser())
|
|
39
|
-
app.use(bodyParser.urlencoded({ extended: false, limit: '50mb' }))
|
|
40
|
-
app.use(bodyParser.json({ limit: '50mb' }))
|
|
41
|
-
|
|
42
|
-
// routing ---------------------------------------------------------
|
|
43
|
-
app.use('/sitemap.xml', sitemapHandler)
|
|
44
|
-
app.use(config.apiPrefix, routes)
|
|
45
|
-
|
|
46
|
-
// error handling --------------------------------------------------\
|
|
47
|
-
|
|
48
|
-
// Apollo engine setup
|
|
49
|
-
const engineConfig = {
|
|
50
|
-
privateVariables: ['files'],
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Apollo server setup
|
|
54
|
-
const apolloServer = new ApolloServer({
|
|
55
|
-
schema,
|
|
56
|
-
context: ({ req }) => {
|
|
57
|
-
if (req.isAuthenticated()) {
|
|
58
|
-
return {
|
|
59
|
-
user: req.user.id,
|
|
60
|
-
isSuperUser: req.user.admin,
|
|
61
|
-
userInfo: req.user,
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
// Always allow introspection - our schema is public
|
|
66
|
-
introspection: true,
|
|
67
|
-
// Enable authenticated queries in playground
|
|
68
|
-
// Note - buggy at the moment
|
|
69
|
-
playground: {
|
|
70
|
-
settings: {
|
|
71
|
-
'request.credentials': 'same-origin',
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
// Enable cache options
|
|
75
|
-
tracing: true,
|
|
76
|
-
cacheControl: true,
|
|
77
|
-
// Don't limit the max size for dataset uploads
|
|
78
|
-
uploads: { maxFieldSize: Infinity },
|
|
79
|
-
formatResponse: response => {
|
|
80
|
-
return { ...response, extensions: { openneuro: { version } } }
|
|
81
|
-
},
|
|
82
|
-
cache: new BaseRedisCache({
|
|
83
|
-
client: redis,
|
|
84
|
-
}),
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
// Setup pre-GraphQL middleware
|
|
88
|
-
app.use('/crn/graphql', jwt.authenticate, auth.optional)
|
|
89
|
-
|
|
90
|
-
// Inject Apollo Server
|
|
91
|
-
apolloServer.applyMiddleware({ app, path: '/crn/graphql' })
|
|
92
|
-
|
|
93
|
-
return app
|
|
94
|
-
}
|