@openneuro/server 4.16.1 → 4.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openneuro/server",
3
- "version": "4.16.1",
3
+ "version": "4.17.1",
4
4
  "description": "Core service for the OpenNeuro platform.",
5
5
  "license": "MIT",
6
6
  "main": "src/server.js",
@@ -17,7 +17,7 @@
17
17
  "dependencies": {
18
18
  "@apollo/client": "3.7.2",
19
19
  "@elastic/elasticsearch": "7.15.0",
20
- "@openneuro/search": "^4.16.1",
20
+ "@openneuro/search": "^4.17.1",
21
21
  "@passport-next/passport-google-oauth2": "^1.0.0",
22
22
  "@sentry/node": "^4.5.3",
23
23
  "apollo-server": "2.25.4",
@@ -29,7 +29,7 @@
29
29
  "date-fns": "^2.16.1",
30
30
  "draft-js": "^0.11.7",
31
31
  "draft-js-export-html": "^1.4.1",
32
- "elastic-apm-node": "^3.31.0",
32
+ "elastic-apm-node": "3.43.0",
33
33
  "express": "^4.17.1",
34
34
  "graphql": "14.7.0",
35
35
  "graphql-bigint": "^1.0.0",
@@ -44,7 +44,7 @@
44
44
  "jsonwebtoken": "^9.0.0",
45
45
  "mime-types": "^2.1.19",
46
46
  "moment": "^2.14.1",
47
- "mongoose": "^6.2.3",
47
+ "mongoose": "^7.0.2",
48
48
  "morgan": "^1.6.1",
49
49
  "node-mailjet": "^3.3.5",
50
50
  "object-hash": "2.1.1",
@@ -92,5 +92,5 @@
92
92
  "publishConfig": {
93
93
  "access": "public"
94
94
  },
95
- "gitHead": "0739211264d035ff4531bf7961232c1abd0165d5"
95
+ "gitHead": "c93f1fd308cddfbaef9990be47e77497a277b35e"
96
96
  }
@@ -20,7 +20,7 @@ describe('pagination model operations', () => {
20
20
  })
21
21
  describe('apiCursor()', () => {
22
22
  it('returns base64 string', () => {
23
- expect(pagination.apiCursor(ObjectID(5))).toMatch(base64)
23
+ expect(pagination.apiCursor(new ObjectID(5))).toMatch(base64)
24
24
  })
25
25
  })
26
26
  describe('applyCursorToEdges()', () => {
@@ -40,7 +40,7 @@ describe('pagination model operations', () => {
40
40
  beforeAll(async () => {
41
41
  await connect(globalThis.__MONGO_URI__)
42
42
  const ds = new Dataset({
43
- _id: ObjectID('5bef51a1ed211400c08e5524'),
43
+ _id: new ObjectID('5bef51a1ed211400c08e5524'),
44
44
  id: 'ds001001',
45
45
  created: new Date('2018-11-16T23:24:17.203Z'),
46
46
  modified: new Date('2018-11-16T23:24:25.050Z'),
@@ -11,7 +11,7 @@ export const followDataset = async (obj, { datasetId }, { user }) => {
11
11
  }
12
12
  if (following) {
13
13
  // unfollow
14
- return following.remove().then(() => ({
14
+ return following.deleteOne().then(() => ({
15
15
  following: false,
16
16
  newFollower,
17
17
  }))
@@ -4,19 +4,19 @@ export const starDataset = async (obj, { datasetId }, { user }) => {
4
4
  const star = await Star.findOne({ datasetId, userId: user }).exec()
5
5
  const newStar = {
6
6
  datasetId,
7
- userId: user
7
+ userId: user,
8
8
  }
9
9
  if (star) {
10
10
  // unstar
11
- return star.remove().then(() => ({
11
+ return star.deleteOne().then(() => ({
12
12
  starred: false,
13
- newStar
13
+ newStar,
14
14
  }))
15
15
  } else {
16
16
  const star = new Star({ datasetId, userId: user })
17
17
  return star.save().then(() => ({
18
18
  starred: true,
19
- newStar
19
+ newStar,
20
20
  }))
21
21
  }
22
22
  }
@@ -18,19 +18,16 @@ export default {
18
18
  const datasetId = data.datasetId
19
19
  const userId = data.userId
20
20
 
21
- Star.create(
22
- {
23
- datasetId: datasetId,
24
- userId: userId,
25
- },
26
- (err, response) => {
27
- if (err) {
28
- return next(err)
29
- } else {
30
- return res.send(response.$op)
31
- }
32
- },
33
- )
21
+ Star.create({
22
+ datasetId: datasetId,
23
+ userId: userId,
24
+ })
25
+ .then(response => {
26
+ return res.send(response.$op)
27
+ })
28
+ .catch(err => {
29
+ next(err)
30
+ })
34
31
  },
35
32
 
36
33
  /**
@@ -72,19 +69,23 @@ export default {
72
69
  if (datasetId) {
73
70
  Star.find({
74
71
  datasetId: datasetId,
75
- }).exec((err, stars) => {
76
- if (err) {
77
- return next(err)
78
- }
79
- res.send(stars)
80
72
  })
73
+ .exec()
74
+ .then(stars => {
75
+ res.send(stars)
76
+ })
77
+ .catch(err => {
78
+ return next(err)
79
+ })
81
80
  } else {
82
- Star.find().exec((err, stars) => {
83
- if (err) {
81
+ Star.find()
82
+ .exec()
83
+ .then(stars => {
84
+ res.send(stars)
85
+ })
86
+ .catch(err => {
84
87
  return next(err)
85
- }
86
- res.send(stars)
87
- })
88
+ })
88
89
  }
89
90
  },
90
91
  }
@@ -59,17 +59,21 @@ export const deleteAll = (req, res, next) => {
59
59
  notifications.datasetDeleted(datasetId)
60
60
  Subscription.find({
61
61
  datasetId: datasetId,
62
- }).exec((err, subscriptions) => {
63
- if (err) {
64
- return next(err)
65
- }
66
- subscriptions.forEach(subscription => {
67
- Subscription.deleteOne({
68
- _id: new ObjectID(subscription._id),
62
+ })
63
+ .exec()
64
+ .then(subscriptions => {
65
+ subscriptions.forEach(subscription => {
66
+ Subscription.deleteOne({
67
+ _id: new ObjectID(subscription._id),
68
+ })
69
69
  })
70
+ return res.send()
71
+ })
72
+ .catch(err => {
73
+ if (err) {
74
+ return next(err)
75
+ }
70
76
  })
71
- return res.send()
72
- })
73
77
  }
74
78
 
75
79
  // read
@@ -85,19 +89,23 @@ export const getSubscriptions = (req, res, next) => {
85
89
  if (datasetId) {
86
90
  Subscription.find({
87
91
  datasetId: datasetId,
88
- }).exec((err, subscriptions) => {
89
- if (err) {
90
- return next(err)
91
- }
92
- res.send(subscriptions)
93
92
  })
93
+ .exec()
94
+ .then(subscriptions => {
95
+ res.send(subscriptions)
96
+ })
97
+ .catch(err => {
98
+ return next(err)
99
+ })
94
100
  } else {
95
- Subscription.find().exec((err, subscriptions) => {
96
- if (err) {
101
+ Subscription.find()
102
+ .exec()
103
+ .then(subscriptions => {
104
+ res.send(subscriptions)
105
+ })
106
+ .catch(err => {
97
107
  return next(err)
98
- }
99
- res.send(subscriptions)
100
- })
108
+ })
101
109
  }
102
110
  }
103
111
 
@@ -115,7 +115,7 @@ export const decodeJWT = token => {
115
115
  return jwt.decode(token)
116
116
  }
117
117
 
118
- const parsedJwtFromRequest = req => {
118
+ export const parsedJwtFromRequest = req => {
119
119
  const jwt = jwtFromRequest(req)
120
120
  if (jwt) return decodeJWT(jwt)
121
121
  else return null
@@ -1,4 +1,6 @@
1
1
  import passport from 'passport'
2
+ import User from '../../models/user'
3
+ import { parsedJwtFromRequest } from './jwt.js'
2
4
 
3
5
  export const requestAuth = passport.authenticate('orcid', {
4
6
  session: false,
@@ -16,7 +18,23 @@ export const authCallback = (req, res, next) =>
16
18
  if (!user) {
17
19
  return res.redirect('/')
18
20
  }
19
- req.logIn(user, { session: false }, err => {
20
- return next(err)
21
- })
21
+ const existingAuth = parsedJwtFromRequest(req)
22
+ if (existingAuth) {
23
+ // Save ORCID to primary account
24
+ User.findOne({ id: existingAuth.sub }, (err, userModel) => {
25
+ if (err) {
26
+ return next(err)
27
+ } else {
28
+ userModel.orcid = user.providerId
29
+ return userModel.save().then(() => {
30
+ res.redirect('/')
31
+ })
32
+ }
33
+ })
34
+ } else {
35
+ // Complete login with ORCID as primary account
36
+ req.logIn(user, { session: false }, err => {
37
+ return next(err)
38
+ })
39
+ }
22
40
  })(req, res, next)
@@ -55,7 +55,7 @@ export const verifyGoogleUser = (accessToken, refreshToken, profile, done) => {
55
55
  profileUpdate,
56
56
  { upsert: true, new: true, setDefaultsOnInsert: true },
57
57
  )
58
- .then(user => done(null, addJWT(config)(user)))
58
+ .then(user => done(null, addJWT(config)(user.toObject())))
59
59
  .catch(err => done(err, null))
60
60
  } else {
61
61
  done(profileUpdate, null)
@@ -80,7 +80,7 @@ export const verifyORCIDUser = (
80
80
  { providerId: profile.orcid, provider: profile.provider },
81
81
  profileUpdate,
82
82
  { upsert: true, new: true, setDefaultsOnInsert: true },
83
- ).then(user => done(null, addJWT(config)(user)))
83
+ ).then(user => done(null, addJWT(config)(user.toObject())))
84
84
  })
85
85
  .catch(err => done(err, null))
86
86
  }
@@ -110,7 +110,7 @@ export const setupPassportAuth = () => {
110
110
  // A user must already exist to use a JWT to auth a request
111
111
  User.findOne({ id: jwt.sub, provider: jwt.provider })
112
112
  .then(user => {
113
- if (user) done(null, user)
113
+ if (user) done(null, user.toObject())
114
114
  else done(null, false)
115
115
  })
116
116
  .catch(done)
@@ -116,54 +116,56 @@ const notifications = {
116
116
  const htmlContent = stateToHTML(contentState)
117
117
 
118
118
  // get all users that are subscribed to the dataset
119
- Subscription.find({ datasetId: datasetId }).exec((err, subscriptions) => {
120
- // create the email object for each user, using subscription userid and scitran
121
- subscriptions.forEach(subscription => {
122
- User.findOne({ id: subscription.userId })
123
- .exec()
124
- .then(user => {
125
- if (user && user.email !== userId) {
126
- const emailContent = {
127
- _id:
128
- datasetId +
129
- '_' +
130
- subscription._id +
131
- '_' +
132
- comment._id +
133
- '_' +
134
- 'comment_created',
135
- type: 'email',
136
- email: {
137
- to: user.email,
138
- name: user.name,
139
- from:
140
- 'reply-' +
141
- encodeURIComponent(comment._id) +
142
- '-' +
143
- encodeURIComponent(user._id),
144
- subject: 'Comment Created',
145
- html: commentCreated({
119
+ Subscription.find({ datasetId: datasetId })
120
+ .exec()
121
+ .then(subscriptions => {
122
+ // create the email object for each user, using subscription userid and scitran
123
+ subscriptions.forEach(subscription => {
124
+ User.findOne({ id: subscription.userId })
125
+ .exec()
126
+ .then(user => {
127
+ if (user && user.email !== userId) {
128
+ const emailContent = {
129
+ _id:
130
+ datasetId +
131
+ '_' +
132
+ subscription._id +
133
+ '_' +
134
+ comment._id +
135
+ '_' +
136
+ 'comment_created',
137
+ type: 'email',
138
+ email: {
139
+ to: user.email,
146
140
  name: user.name,
147
- datasetName: bidsId.decodeId(datasetId),
148
- datasetLabel: datasetLabel,
149
- commentUserId: userId,
150
- commentId: commentId,
151
- dateCreated: moment(comment.createDate).format('MMMM Do'),
152
- commentContent: htmlContent,
153
- commentStatus: commentStatus,
154
- siteUrl:
155
- url.parse(config.url).protocol +
156
- '//' +
157
- url.parse(config.url).hostname,
158
- }),
159
- },
141
+ from:
142
+ 'reply-' +
143
+ encodeURIComponent(comment._id) +
144
+ '-' +
145
+ encodeURIComponent(user._id),
146
+ subject: 'Comment Created',
147
+ html: commentCreated({
148
+ name: user.name,
149
+ datasetName: bidsId.decodeId(datasetId),
150
+ datasetLabel: datasetLabel,
151
+ commentUserId: userId,
152
+ commentId: commentId,
153
+ dateCreated: moment(comment.createDate).format('MMMM Do'),
154
+ commentContent: htmlContent,
155
+ commentStatus: commentStatus,
156
+ siteUrl:
157
+ url.parse(config.url).protocol +
158
+ '//' +
159
+ url.parse(config.url).hostname,
160
+ }),
161
+ },
162
+ }
163
+ // send each email to the notification database for distribution
164
+ notifications.send(emailContent)
160
165
  }
161
- // send each email to the notification database for distribution
162
- notifications.send(emailContent)
163
- }
164
- })
166
+ })
167
+ })
165
168
  })
166
- })
167
169
  },
168
170
 
169
171
  /**
@@ -180,36 +182,42 @@ const notifications = {
180
182
  )
181
183
 
182
184
  // get all users that are subscribed to the dataset
183
- Subscription.find({ datasetId: datasetId }).exec((err, subscriptions) => {
184
- // create the email object for each user, using subscription userid and scitran
185
- subscriptions.forEach(subscription => {
186
- User.findOne({ id: subscription.userId })
187
- .exec()
188
- .then(user => {
189
- if (user) {
190
- const emailContent = {
191
- _id:
192
- datasetId + '_' + subscription._id + '_' + 'dataset_deleted',
193
- type: 'email',
194
- email: {
195
- to: user.email,
196
- name: user.name,
197
- subject: 'Dataset Deleted',
198
- html: datasetDeleted({
185
+ Subscription.find({ datasetId: datasetId })
186
+ .exec()
187
+ .then(subscriptions => {
188
+ // create the email object for each user, using subscription userid and scitran
189
+ subscriptions.forEach(subscription => {
190
+ User.findOne({ id: subscription.userId })
191
+ .exec()
192
+ .then(user => {
193
+ if (user) {
194
+ const emailContent = {
195
+ _id:
196
+ datasetId +
197
+ '_' +
198
+ subscription._id +
199
+ '_' +
200
+ 'dataset_deleted',
201
+ type: 'email',
202
+ email: {
203
+ to: user.email,
199
204
  name: user.name,
200
- datasetName: bidsId.decodeId(datasetId),
201
- siteUrl:
202
- url.parse(config.url).protocol +
203
- '//' +
204
- url.parse(config.url).hostname,
205
- }),
206
- },
205
+ subject: 'Dataset Deleted',
206
+ html: datasetDeleted({
207
+ name: user.name,
208
+ datasetName: bidsId.decodeId(datasetId),
209
+ siteUrl:
210
+ url.parse(config.url).protocol +
211
+ '//' +
212
+ url.parse(config.url).hostname,
213
+ }),
214
+ },
215
+ }
216
+ notifications.send(emailContent)
207
217
  }
208
- notifications.send(emailContent)
209
- }
210
- })
218
+ })
219
+ })
211
220
  })
212
- })
213
221
  },
214
222
 
215
223
  /**
@@ -226,40 +234,42 @@ const notifications = {
226
234
  )
227
235
 
228
236
  // get all users that are subscribed to the dataset
229
- Subscription.find({ datasetId: datasetId }).exec((err, subscriptions) => {
230
- // create the email object for each user, using subscription userid and scitran
231
- subscriptions.forEach(subscription => {
232
- User.findOne({ id: subscription.userId })
233
- .exec()
234
- .then(user => {
235
- if (user) {
236
- const emailContent = {
237
- _id:
238
- datasetId +
239
- '_' +
240
- subscription._id +
241
- '_' +
242
- 'owner_unsubscribed',
243
- type: 'email',
244
- email: {
245
- to: user.email,
246
- name: user.name,
247
- subject: 'Owner Unsubscribed',
248
- html: ownerUnsubscribed({
237
+ Subscription.find({ datasetId: datasetId })
238
+ .exec()
239
+ .then(subscriptions => {
240
+ // create the email object for each user, using subscription userid and scitran
241
+ subscriptions.forEach(subscription => {
242
+ User.findOne({ id: subscription.userId })
243
+ .exec()
244
+ .then(user => {
245
+ if (user) {
246
+ const emailContent = {
247
+ _id:
248
+ datasetId +
249
+ '_' +
250
+ subscription._id +
251
+ '_' +
252
+ 'owner_unsubscribed',
253
+ type: 'email',
254
+ email: {
255
+ to: user.email,
249
256
  name: user.name,
250
- datasetName: bidsId.decodeId(datasetId),
251
- siteUrl:
252
- url.parse(config.url).protocol +
253
- '//' +
254
- url.parse(config.url).hostname,
255
- }),
256
- },
257
+ subject: 'Owner Unsubscribed',
258
+ html: ownerUnsubscribed({
259
+ name: user.name,
260
+ datasetName: bidsId.decodeId(datasetId),
261
+ siteUrl:
262
+ url.parse(config.url).protocol +
263
+ '//' +
264
+ url.parse(config.url).hostname,
265
+ }),
266
+ },
267
+ }
268
+ notifications.send(emailContent)
257
269
  }
258
- notifications.send(emailContent)
259
- }
260
- })
270
+ })
271
+ })
261
272
  })
262
- })
263
273
  },
264
274
 
265
275
  /**
@@ -276,8 +286,9 @@ const notifications = {
276
286
  )
277
287
 
278
288
  // get all users that are subscribed to the dataset
279
- await Subscription.find({ datasetId: datasetId }).exec(
280
- (err, subscriptions) => {
289
+ await Subscription.find({ datasetId: datasetId })
290
+ .exec()
291
+ .then(subscriptions => {
281
292
  // create the email object for each user, using subscription userid and scitran
282
293
  subscriptions.forEach(subscription => {
283
294
  User.findOne({ id: subscription.userId })
@@ -312,8 +323,7 @@ const notifications = {
312
323
  }
313
324
  })
314
325
  })
315
- },
316
- )
326
+ })
317
327
  },
318
328
 
319
329
  /**
package/src/libs/orcid.js CHANGED
@@ -20,12 +20,22 @@ export default {
20
20
  },
21
21
  (err, res) => {
22
22
  if (err) {
23
- reject({
23
+ return reject({
24
24
  message:
25
25
  'An unexpected ORCID login failure occurred, please try again later.',
26
26
  })
27
27
  }
28
- const doc = new xmldoc.XmlDocument(res.body)
28
+ let doc
29
+ // Catch issues with parsing this response
30
+ try {
31
+ doc = new xmldoc.XmlDocument(res.body)
32
+ } catch (err) {
33
+ return reject({
34
+ type: 'config',
35
+ message:
36
+ 'ORCID auth response invalid, most likely this is a misconfigured ORCID_API_ENDPOINT value',
37
+ })
38
+ }
29
39
  let name = doc.valueWithPath(
30
40
  'person:person.person:name.personal-details:credit-name',
31
41
  )
@@ -41,13 +51,13 @@ export default {
41
51
 
42
52
  if (!name) {
43
53
  if (!firstname) {
44
- reject({
54
+ return reject({
45
55
  type: 'given',
46
56
  message:
47
57
  'Your ORCID account does not have a given name, or it is not public. Please fix your account before continuing.',
48
58
  })
49
59
  } else if (!lastname) {
50
- reject({
60
+ return reject({
51
61
  type: 'family',
52
62
  message:
53
63
  'Your ORCID account does not have a family name, or it is not public. Please fix your account before continuing.',
@@ -58,7 +68,7 @@ export default {
58
68
  }
59
69
 
60
70
  if (!email) {
61
- reject({
71
+ return reject({
62
72
  type: 'email',
63
73
  message:
64
74
  'Your ORCID account does not have an e-mail, or your e-mail is not public. Please fix your account before continuing.',
@@ -6,9 +6,8 @@ vi.mock('ioredis')
6
6
  describe('IngestDataset model', () => {
7
7
  it('IngestDataset model fails if required fields are missing', () => {
8
8
  const model = new IngestDataset()
9
- model.validate(result => {
10
- expect(result.name).toEqual('ValidationError')
11
- })
9
+ const result = model.validateSync()
10
+ expect(result.name).toEqual('ValidationError')
12
11
  })
13
12
  it('IngestDataset model URL validation fails with a bad URL', async () => {
14
13
  const badUrlModel = new IngestDataset({
@@ -18,9 +17,8 @@ describe('IngestDataset model', () => {
18
17
  imported: false,
19
18
  notified: false,
20
19
  })
21
- await badUrlModel.validate(result => {
22
- expect(result.name).toEqual('ValidationError')
23
- })
20
+ const result = badUrlModel.validateSync()
21
+ expect(result.name).toEqual('ValidationError')
24
22
  })
25
23
  it('IngestDataset model URL validation succeeds with a good URL', async () => {
26
24
  const goodUrlModel = new IngestDataset({
@@ -30,8 +28,7 @@ describe('IngestDataset model', () => {
30
28
  imported: false,
31
29
  notified: false,
32
30
  })
33
- await goodUrlModel.validate(result => {
34
- expect(result).toBe(undefined)
35
- })
31
+ const result = goodUrlModel.validateSync()
32
+ expect(result).toBe(undefined)
36
33
  })
37
34
  })
@@ -74,24 +74,17 @@ datasetSchema.virtual('subscriptions', {
74
74
  justOne: true,
75
75
  })
76
76
 
77
- datasetSchema.post('save', dataset => {
78
- new DatasetChange({
79
- datasetId: dataset.id,
80
- created: true,
81
- }).save()
82
- })
83
-
84
77
  datasetSchema.post('updateOne', function () {
85
- const datasetId = this._conditions ? this._conditions.id : null
86
- new DatasetChange({
78
+ const datasetId = this.getQuery()?.['id']
79
+ return new DatasetChange({
87
80
  datasetId,
88
81
  modified: true,
89
82
  }).save()
90
83
  })
91
84
 
92
85
  datasetSchema.post('deleteOne', function () {
93
- const datasetId = this._conditions ? this._conditions.id : null
94
- new DatasetChange({
86
+ const datasetId = this.getQuery()?.['id']
87
+ return new DatasetChange({
95
88
  datasetId,
96
89
  deleted: true,
97
90
  }).save()