@openneuro/server 4.20.4 → 4.20.6-alpha.2
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 +4 -6
- package/src/__mocks__/{config.js → config.ts} +5 -5
- package/src/app.ts +32 -31
- package/src/cache/item.ts +6 -7
- package/src/cache/types.ts +8 -8
- package/src/{config.js → config.ts} +6 -6
- package/src/datalad/__tests__/changelog.spec.ts +83 -0
- package/src/datalad/__tests__/dataset.spec.ts +109 -0
- package/src/datalad/__tests__/description.spec.ts +141 -0
- package/src/datalad/__tests__/files.spec.ts +77 -0
- package/src/datalad/__tests__/pagination.spec.ts +136 -0
- package/src/datalad/__tests__/{snapshots.spec.js → snapshots.spec.ts} +17 -17
- package/src/datalad/{analytics.js → analytics.ts} +4 -4
- package/src/datalad/{changelog.js → changelog.ts} +17 -14
- package/src/datalad/{dataset.js → dataset.ts} +95 -93
- package/src/datalad/{description.js → description.ts} +37 -37
- package/src/datalad/draft.ts +38 -0
- package/src/datalad/files.ts +26 -20
- package/src/datalad/{pagination.js → pagination.ts} +47 -47
- package/src/datalad/{readme.js → readme.ts} +13 -11
- package/src/datalad/{reexporter.js → reexporter.ts} +4 -4
- package/src/datalad/{snapshots.js → snapshots.ts} +56 -62
- package/src/datalad/{upload.js → upload.ts} +7 -5
- package/src/elasticsearch/elastic-client.ts +11 -0
- package/src/elasticsearch/reindex-dataset.ts +7 -7
- package/src/graphql/__tests__/__snapshots__/permissions.spec.ts.snap +5 -0
- package/src/graphql/__tests__/{comment.spec.js → comment.spec.ts} +17 -17
- package/src/graphql/__tests__/permissions.spec.ts +113 -0
- package/src/graphql/{permissions.js → permissions.ts} +14 -14
- package/src/graphql/resolvers/__tests__/brainlife.spec.ts +11 -11
- package/src/graphql/resolvers/__tests__/{dataset-search.spec.js → dataset-search.spec.ts} +25 -23
- package/src/graphql/resolvers/__tests__/dataset.spec.ts +175 -0
- package/src/graphql/resolvers/__tests__/derivatives.spec.ts +19 -19
- package/src/graphql/resolvers/__tests__/importRemoteDataset.spec.ts +20 -20
- package/src/graphql/resolvers/__tests__/permssions.spec.ts +35 -0
- package/src/graphql/resolvers/__tests__/snapshots.spec.ts +59 -0
- package/src/graphql/resolvers/__tests__/user.spec.ts +18 -0
- package/src/graphql/resolvers/brainlife.ts +4 -4
- package/src/graphql/resolvers/cache.ts +4 -4
- package/src/graphql/resolvers/{comment.js → comment.ts} +16 -16
- package/src/graphql/resolvers/{dataset-search.js → dataset-search.ts} +45 -43
- package/src/graphql/resolvers/{dataset.js → dataset.ts} +38 -52
- package/src/graphql/resolvers/datasetType.ts +3 -3
- package/src/graphql/resolvers/derivatives.ts +11 -11
- package/src/graphql/resolvers/description.ts +18 -0
- package/src/graphql/resolvers/{draft.js → draft.ts} +13 -13
- package/src/graphql/resolvers/{flaggedFiles.js → flaggedFiles.ts} +4 -4
- package/src/graphql/resolvers/{follow.js → follow.ts} +1 -1
- package/src/graphql/resolvers/git.ts +3 -3
- package/src/graphql/resolvers/history.ts +13 -0
- package/src/graphql/resolvers/importRemoteDataset.ts +12 -11
- package/src/graphql/resolvers/index.ts +25 -0
- package/src/graphql/resolvers/{issues.js → issues.ts} +9 -9
- package/src/graphql/resolvers/metadata.ts +8 -8
- package/src/graphql/resolvers/{mutation.js → mutation.ts} +26 -26
- package/src/graphql/resolvers/{newsletter.js → newsletter.ts} +2 -2
- package/src/graphql/resolvers/permissions.ts +15 -21
- package/src/graphql/resolvers/publish.ts +17 -0
- package/src/graphql/resolvers/query.ts +21 -0
- package/src/graphql/resolvers/{readme.js → readme.ts} +3 -3
- package/src/graphql/resolvers/{reexporter.js → reexporter.ts} +2 -2
- package/src/graphql/resolvers/relation.ts +5 -5
- package/src/graphql/resolvers/{reset.js → reset.ts} +2 -2
- package/src/graphql/resolvers/reviewer.ts +4 -4
- package/src/graphql/resolvers/{snapshots.js → snapshots.ts} +49 -49
- package/src/graphql/resolvers/{stars.js → stars.ts} +1 -1
- package/src/graphql/resolvers/summary.ts +3 -3
- package/src/graphql/resolvers/{upload.js → upload.ts} +5 -5
- package/src/graphql/resolvers/{user.js → user.ts} +16 -18
- package/src/graphql/resolvers/{validation.js → validation.ts} +12 -14
- package/src/graphql/{schema.js → schema.ts} +4 -6
- package/src/graphql/utils/{file.js → file.ts} +2 -2
- package/src/handlers/{comments.js → comments.ts} +11 -11
- package/src/handlers/{config.js → config.ts} +1 -1
- package/src/handlers/{datalad.js → datalad.ts} +22 -22
- package/src/handlers/{doi.js → doi.ts} +6 -6
- package/src/handlers/reviewer.ts +6 -6
- package/src/handlers/{sitemap.js → sitemap.ts} +19 -19
- package/src/handlers/stars.ts +11 -10
- package/src/handlers/{subscriptions.js → subscriptions.ts} +17 -16
- package/src/handlers/{users.js → users.ts} +3 -3
- package/src/libs/__tests__/apikey.spec.ts +25 -0
- package/src/libs/__tests__/datalad-service.spec.ts +27 -0
- package/src/libs/__tests__/{dataset.spec.js → dataset.spec.ts} +9 -9
- package/src/libs/{apikey.js → apikey.ts} +5 -5
- package/src/libs/authentication/__tests__/jwt.spec.ts +59 -0
- package/src/libs/authentication/{crypto.js → crypto.ts} +16 -16
- package/src/libs/authentication/google.ts +18 -0
- package/src/libs/authentication/jwt.ts +40 -33
- package/src/libs/authentication/{orcid.js → orcid.ts} +11 -11
- package/src/libs/authentication/{passport.js → passport.ts} +45 -30
- package/src/libs/authentication/{states.js → states.ts} +17 -20
- package/src/libs/{counter.js → counter.ts} +1 -1
- package/src/libs/{datalad-service.js → datalad-service.ts} +4 -4
- package/src/libs/dataset.ts +9 -0
- package/src/libs/doi/__tests__/__snapshots__/doi.spec.ts.snap +17 -0
- package/src/libs/doi/__tests__/doi.spec.ts +25 -0
- package/src/libs/doi/__tests__/normalize.spec.ts +19 -19
- package/src/libs/doi/{index.js → index.ts} +27 -21
- package/src/libs/doi/normalize.ts +2 -2
- package/src/libs/email/__tests__/index.spec.ts +14 -14
- package/src/libs/email/index.ts +4 -4
- package/src/libs/email/templates/__tests__/comment-created.spec.ts +12 -12
- package/src/libs/email/templates/__tests__/dataset-deleted.spec.ts +6 -6
- package/src/libs/email/templates/__tests__/owner-unsubscribed.spec.ts +6 -6
- package/src/libs/email/templates/__tests__/snapshot-created.spec.ts +9 -9
- package/src/libs/email/templates/__tests__/snapshot-reminder.spec.ts +7 -7
- package/src/libs/email/templates/comment-created.ts +2 -1
- package/src/libs/email/templates/dataset-deleted.ts +2 -1
- package/src/libs/email/templates/dataset-import-failed.ts +2 -1
- package/src/libs/email/templates/dataset-imported.ts +2 -1
- package/src/libs/email/templates/owner-unsubscribed.ts +2 -1
- package/src/libs/email/templates/snapshot-created.ts +2 -1
- package/src/libs/email/templates/snapshot-reminder.ts +2 -1
- package/src/libs/{notifications.js → notifications.ts} +100 -113
- package/src/libs/{orcid.js → orcid.ts} +20 -20
- package/src/libs/{redis.js → redis.ts} +6 -6
- package/src/models/__tests__/ingestDataset.spec.ts +15 -15
- package/src/models/analytics.ts +2 -2
- package/src/models/badAnnexObject.ts +6 -6
- package/src/models/comment.ts +10 -10
- package/src/models/counter.ts +2 -2
- package/src/models/dataset.ts +16 -16
- package/src/models/deletion.ts +3 -3
- package/src/models/deprecatedSnapshot.ts +2 -2
- package/src/models/doi.ts +2 -2
- package/src/models/file.ts +2 -2
- package/src/models/ingestDataset.ts +4 -4
- package/src/models/issue.ts +2 -2
- package/src/models/key.ts +2 -2
- package/src/models/mailgunIdentifier.ts +2 -2
- package/src/models/metadata.ts +3 -3
- package/src/models/newsletter.ts +3 -3
- package/src/models/notification.ts +2 -2
- package/src/models/permission.ts +4 -4
- package/src/models/reviewer.ts +7 -7
- package/src/models/snapshot.ts +2 -2
- package/src/models/stars.ts +6 -6
- package/src/models/subscription.ts +2 -2
- package/src/models/summary.ts +2 -2
- package/src/models/upload.ts +3 -3
- package/src/models/user.ts +4 -4
- package/src/{routes.js → routes.ts} +62 -62
- package/src/server.ts +9 -9
- package/src/utils/__tests__/datasetOrSnapshot.spec.ts +25 -25
- package/src/utils/__tests__/validateUrl.spec.ts +10 -10
- package/src/utils/datasetOrSnapshot.ts +2 -2
- package/src/utils/validateUrl.ts +1 -1
- package/src/datalad/__tests__/changelog.spec.js +0 -82
- package/src/datalad/__tests__/dataset.spec.js +0 -109
- package/src/datalad/__tests__/description.spec.js +0 -137
- package/src/datalad/__tests__/files.spec.js +0 -75
- package/src/datalad/__tests__/pagination.spec.js +0 -136
- package/src/datalad/draft.js +0 -37
- package/src/elasticsearch/elastic-client.js +0 -11
- package/src/graphql/__tests__/permissions.spec.js +0 -107
- package/src/graphql/pubsub.js +0 -5
- package/src/graphql/resolvers/__tests__/dataset.spec.js +0 -175
- package/src/graphql/resolvers/__tests__/permssions.spec.js +0 -34
- package/src/graphql/resolvers/__tests__/snapshots.spec.js +0 -58
- package/src/graphql/resolvers/__tests__/user.spec.js +0 -17
- package/src/graphql/resolvers/description.js +0 -29
- package/src/graphql/resolvers/history.js +0 -11
- package/src/graphql/resolvers/index.js +0 -25
- package/src/graphql/resolvers/publish.js +0 -17
- package/src/graphql/resolvers/query.js +0 -21
- package/src/graphql/resolvers/subscriptions.js +0 -81
- package/src/graphql/utils/publish-draft-update.js +0 -13
- package/src/libs/__tests__/apikey.spec.js +0 -24
- package/src/libs/__tests__/datalad-service.spec.js +0 -26
- package/src/libs/authentication/__tests__/jwt.spec.js +0 -23
- package/src/libs/authentication/globus.js +0 -11
- package/src/libs/authentication/google.js +0 -19
- package/src/libs/bidsId.js +0 -68
- package/src/libs/dataset.js +0 -9
- package/src/libs/doi/__tests__/doi.spec.js +0 -24
- package/src/libs/redis-pubsub.js +0 -5
- package/src/libs/request.js +0 -155
- package/src/libs/scitran.js +0 -25
- package/src/libs/subscription-server.js +0 -20
- package/src/libs/testing-utils.js +0 -17
- package/src/persistent/datasets/.gitignore +0 -3
- package/src/persistent/temp/.gitignore +0 -3
- /package/src/libs/__mocks__/{notifications.js → notifications.ts} +0 -0
- /package/src/libs/authentication/{verifyUser.js → verifyUser.ts} +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import passport from
|
|
2
|
-
import refresh from
|
|
3
|
-
import jwt from
|
|
4
|
-
import { decrypt } from
|
|
5
|
-
import User from
|
|
6
|
-
import config from
|
|
1
|
+
import passport from "passport"
|
|
2
|
+
import refresh from "passport-oauth2-refresh"
|
|
3
|
+
import jwt from "jsonwebtoken"
|
|
4
|
+
import { decrypt } from "./crypto"
|
|
5
|
+
import User from "../../models/user"
|
|
6
|
+
import config from "../../config"
|
|
7
7
|
|
|
8
8
|
interface OpenNeuroTokenProfile {
|
|
9
9
|
sub: string
|
|
@@ -24,7 +24,7 @@ export const buildToken = (
|
|
|
24
24
|
expiresIn,
|
|
25
25
|
options?: { scopes?: string[]; dataset?: string },
|
|
26
26
|
): string => {
|
|
27
|
-
const fields: Omit<OpenNeuroTokenProfile,
|
|
27
|
+
const fields: Omit<OpenNeuroTokenProfile, "iat" | "exp"> = {
|
|
28
28
|
sub: user.id,
|
|
29
29
|
email: user.email,
|
|
30
30
|
provider: user.provider,
|
|
@@ -33,10 +33,10 @@ export const buildToken = (
|
|
|
33
33
|
}
|
|
34
34
|
// Allow extensions of the base token format
|
|
35
35
|
if (options) {
|
|
36
|
-
if (options &&
|
|
36
|
+
if (options && "scopes" in options) {
|
|
37
37
|
fields.scopes = options.scopes
|
|
38
38
|
}
|
|
39
|
-
if (
|
|
39
|
+
if ("dataset" in options) {
|
|
40
40
|
fields.dataset = options.dataset
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -46,12 +46,10 @@ export const buildToken = (
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// Helper to generate a JWT containing user info
|
|
49
|
-
export const addJWT =
|
|
50
|
-
config
|
|
51
|
-
(user,
|
|
52
|
-
|
|
53
|
-
return Object.assign({}, user, { token })
|
|
54
|
-
}
|
|
49
|
+
export const addJWT = (config) => (user, expiration = 60 * 60 * 24 * 7) => {
|
|
50
|
+
const token = buildToken(config, user, expiration)
|
|
51
|
+
return Object.assign({}, user, { token })
|
|
52
|
+
}
|
|
55
53
|
|
|
56
54
|
/**
|
|
57
55
|
* Generate an upload specific token
|
|
@@ -64,7 +62,7 @@ export function generateUploadToken(
|
|
|
64
62
|
expiresIn = 60 * 60 * 24 * 7,
|
|
65
63
|
) {
|
|
66
64
|
const options = {
|
|
67
|
-
scopes: [
|
|
65
|
+
scopes: ["dataset:upload"],
|
|
68
66
|
dataset: datasetId,
|
|
69
67
|
}
|
|
70
68
|
return buildToken(config, user, expiresIn, options)
|
|
@@ -79,14 +77,14 @@ export function generateReviewerToken(
|
|
|
79
77
|
expiresIn = 60 * 60 * 24 * 365,
|
|
80
78
|
) {
|
|
81
79
|
const options = {
|
|
82
|
-
scopes: [
|
|
80
|
+
scopes: ["dataset:reviewer"],
|
|
83
81
|
dataset: datasetId,
|
|
84
82
|
}
|
|
85
83
|
const reviewer = {
|
|
86
84
|
id,
|
|
87
|
-
email:
|
|
88
|
-
provider:
|
|
89
|
-
name:
|
|
85
|
+
email: "reviewer@openneuro.org",
|
|
86
|
+
provider: "OpenNeuro",
|
|
87
|
+
name: "Anonymous Reviewer",
|
|
90
88
|
admin: false,
|
|
91
89
|
}
|
|
92
90
|
return buildToken(config, reviewer, expiresIn, options)
|
|
@@ -99,7 +97,7 @@ export function generateReviewerToken(
|
|
|
99
97
|
*/
|
|
100
98
|
export function generateRepoToken(user, datasetId, expiresIn = 60 * 60 * 24) {
|
|
101
99
|
const options = {
|
|
102
|
-
scopes: [
|
|
100
|
+
scopes: ["dataset:git"],
|
|
103
101
|
dataset: datasetId,
|
|
104
102
|
}
|
|
105
103
|
return buildToken(config, user, expiresIn, options)
|
|
@@ -121,8 +119,17 @@ const requestNewAccessToken = (jwtProvider, refreshToken) =>
|
|
|
121
119
|
* Extract the JWT from a cookie
|
|
122
120
|
* @param {Object} req
|
|
123
121
|
*/
|
|
124
|
-
export const jwtFromRequest = req => {
|
|
125
|
-
if (req.
|
|
122
|
+
export const jwtFromRequest = (req) => {
|
|
123
|
+
if (req.headers?.authorization) {
|
|
124
|
+
try {
|
|
125
|
+
return req.headers.authorization.substring(
|
|
126
|
+
7,
|
|
127
|
+
req.headers.authorization.length,
|
|
128
|
+
)
|
|
129
|
+
} catch (_err) {
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
} else if (req.cookies && req.cookies.accessToken) {
|
|
126
133
|
return req.cookies.accessToken
|
|
127
134
|
} else {
|
|
128
135
|
return null
|
|
@@ -133,13 +140,13 @@ export const decodeJWT = (token: string): OpenNeuroTokenProfile => {
|
|
|
133
140
|
return jwt.decode(token) as OpenNeuroTokenProfile
|
|
134
141
|
}
|
|
135
142
|
|
|
136
|
-
export const parsedJwtFromRequest = req => {
|
|
143
|
+
export const parsedJwtFromRequest = (req) => {
|
|
137
144
|
const jwt = jwtFromRequest(req)
|
|
138
145
|
if (jwt) return decodeJWT(jwt)
|
|
139
146
|
else return null
|
|
140
147
|
}
|
|
141
148
|
|
|
142
|
-
const refreshToken = async jwt => {
|
|
149
|
+
const refreshToken = async (jwt) => {
|
|
143
150
|
const user = await User.findOne({ id: jwt.sub, provider: jwt.provider })
|
|
144
151
|
if (user && user.refresh) {
|
|
145
152
|
const refreshToken = decrypt(user.refresh)
|
|
@@ -152,7 +159,7 @@ const refreshToken = async jwt => {
|
|
|
152
159
|
}
|
|
153
160
|
|
|
154
161
|
// Shared options for Express response.cookie()
|
|
155
|
-
const cookieOptions = { sameSite:
|
|
162
|
+
const cookieOptions = { sameSite: "Lax" }
|
|
156
163
|
|
|
157
164
|
// attach user obj to request based on jwt
|
|
158
165
|
// if user does not exist, continue
|
|
@@ -163,10 +170,10 @@ export const authenticate = (req, res, next) => {
|
|
|
163
170
|
const token = await refreshToken(jwt)
|
|
164
171
|
if (token) {
|
|
165
172
|
req.cookies.accessToken = token
|
|
166
|
-
res.cookie(
|
|
173
|
+
res.cookie("accessToken", token, cookieOptions)
|
|
167
174
|
}
|
|
168
175
|
}
|
|
169
|
-
passport.authenticate(
|
|
176
|
+
passport.authenticate("jwt", { session: false }, (err, user) => {
|
|
170
177
|
req.login(user, { session: false }, () => next())
|
|
171
178
|
})(req, res, next)
|
|
172
179
|
}
|
|
@@ -175,11 +182,11 @@ export const authenticate = (req, res, next) => {
|
|
|
175
182
|
|
|
176
183
|
export const authSuccessHandler = (req, res, next) => {
|
|
177
184
|
const redirectPath = req.query.state
|
|
178
|
-
? Buffer.from(req.query.state,
|
|
179
|
-
:
|
|
185
|
+
? Buffer.from(req.query.state, "base64").toString()
|
|
186
|
+
: "/"
|
|
180
187
|
if (req.user) {
|
|
181
188
|
// Set the JWT associated with this login on a cookie
|
|
182
|
-
res.cookie(
|
|
189
|
+
res.cookie("accessToken", req.user.token, cookieOptions)
|
|
183
190
|
res.redirect(redirectPath)
|
|
184
191
|
} else {
|
|
185
192
|
res.status(401)
|
|
@@ -187,6 +194,6 @@ export const authSuccessHandler = (req, res, next) => {
|
|
|
187
194
|
return next()
|
|
188
195
|
}
|
|
189
196
|
|
|
190
|
-
export const generateDataladCookie = config => user => {
|
|
191
|
-
return user ? `accessToken=${addJWT(config)(user).token}` :
|
|
197
|
+
export const generateDataladCookie = (config) => (user) => {
|
|
198
|
+
return user ? `accessToken=${addJWT(config)(user).token}` : ""
|
|
192
199
|
}
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
import passport from
|
|
2
|
-
import User from
|
|
3
|
-
import { parsedJwtFromRequest } from
|
|
1
|
+
import passport from "passport"
|
|
2
|
+
import User from "../../models/user"
|
|
3
|
+
import { parsedJwtFromRequest } from "./jwt"
|
|
4
4
|
|
|
5
|
-
export const requestAuth = passport.authenticate(
|
|
5
|
+
export const requestAuth = passport.authenticate("orcid", {
|
|
6
6
|
session: false,
|
|
7
7
|
})
|
|
8
8
|
|
|
9
9
|
export const authCallback = (req, res, next) =>
|
|
10
|
-
passport.authenticate(
|
|
10
|
+
passport.authenticate("orcid", (err, user) => {
|
|
11
11
|
if (err) {
|
|
12
12
|
if (err.type) {
|
|
13
13
|
return res.redirect(`/error/orcid/${err.type}`)
|
|
14
14
|
} else {
|
|
15
|
-
return res.redirect(
|
|
15
|
+
return res.redirect("/error/orcid/unknown")
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
if (!user) {
|
|
19
|
-
return res.redirect(
|
|
19
|
+
return res.redirect("/")
|
|
20
20
|
}
|
|
21
21
|
const existingAuth = parsedJwtFromRequest(req)
|
|
22
22
|
if (existingAuth) {
|
|
23
23
|
// Save ORCID to primary account
|
|
24
24
|
User.findOne({ id: existingAuth.sub })
|
|
25
|
-
.then(userModel => {
|
|
25
|
+
.then((userModel) => {
|
|
26
26
|
userModel.orcid = user.providerId
|
|
27
27
|
return userModel.save().then(() => {
|
|
28
|
-
res.redirect(
|
|
28
|
+
res.redirect("/")
|
|
29
29
|
})
|
|
30
30
|
})
|
|
31
|
-
.catch(err => {
|
|
31
|
+
.catch((err) => {
|
|
32
32
|
return next(err)
|
|
33
33
|
})
|
|
34
34
|
} else {
|
|
35
35
|
// Complete login with ORCID as primary account
|
|
36
|
-
req.logIn(user, { session: false }, err => {
|
|
36
|
+
req.logIn(user, { session: false }, (err) => {
|
|
37
37
|
return next(err)
|
|
38
38
|
})
|
|
39
39
|
}
|
|
@@ -1,30 +1,40 @@
|
|
|
1
|
-
import passport from
|
|
2
|
-
import refresh from
|
|
3
|
-
import { Strategy as JwtStrategy } from
|
|
4
|
-
import { Strategy as GoogleStrategy } from
|
|
5
|
-
import { Strategy as ORCIDStrategy } from
|
|
6
|
-
import config from
|
|
7
|
-
import User from
|
|
8
|
-
import { encrypt } from
|
|
9
|
-
import { addJWT, jwtFromRequest } from
|
|
10
|
-
import orcid from
|
|
1
|
+
import passport from "passport"
|
|
2
|
+
import refresh from "passport-oauth2-refresh"
|
|
3
|
+
import { Strategy as JwtStrategy } from "passport-jwt"
|
|
4
|
+
import { Strategy as GoogleStrategy } from "passport-google-oauth20"
|
|
5
|
+
import { Strategy as ORCIDStrategy } from "passport-orcid"
|
|
6
|
+
import config from "../../config"
|
|
7
|
+
import User from "../../models/user"
|
|
8
|
+
import { encrypt } from "./crypto"
|
|
9
|
+
import { addJWT, jwtFromRequest } from "./jwt"
|
|
10
|
+
import orcid from "../orcid"
|
|
11
11
|
|
|
12
12
|
const PROVIDERS = {
|
|
13
|
-
GOOGLE:
|
|
14
|
-
ORCID:
|
|
13
|
+
GOOGLE: "google",
|
|
14
|
+
ORCID: "orcid",
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
interface OauthProfile {
|
|
18
|
+
email: string
|
|
19
|
+
name: string
|
|
20
|
+
provider: string
|
|
21
|
+
providerId: string
|
|
22
|
+
orcid?: string
|
|
23
|
+
refresh?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const loadProfile = (profile): OauthProfile | Error => {
|
|
18
27
|
if (profile.provider === PROVIDERS.GOOGLE) {
|
|
19
28
|
// Get the account email from Google profile
|
|
20
29
|
const primaryEmail = profile.emails
|
|
21
|
-
.filter(email => email.verified === true)
|
|
30
|
+
.filter((email) => email.verified === true)
|
|
22
31
|
.shift()
|
|
23
32
|
return {
|
|
24
33
|
email: primaryEmail.value,
|
|
25
34
|
name: profile.displayName,
|
|
26
35
|
provider: profile.provider,
|
|
27
36
|
providerId: profile.id,
|
|
37
|
+
refresh: undefined,
|
|
28
38
|
}
|
|
29
39
|
} else if (profile.provider === PROVIDERS.ORCID) {
|
|
30
40
|
return {
|
|
@@ -33,19 +43,21 @@ const loadProfile = profile => {
|
|
|
33
43
|
provider: profile.provider,
|
|
34
44
|
providerId: profile.orcid,
|
|
35
45
|
orcid: profile.orcid,
|
|
46
|
+
refresh: undefined,
|
|
36
47
|
}
|
|
37
48
|
} else {
|
|
38
49
|
// Some unknown profile type
|
|
39
|
-
return new Error(
|
|
50
|
+
return new Error("Unhandled profile type.")
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
export const verifyGoogleUser = (accessToken, refreshToken, profile, done) => {
|
|
44
55
|
const profileUpdate = loadProfile(profile)
|
|
45
|
-
if (refreshToken && !(profileUpdate instanceof Error))
|
|
56
|
+
if (refreshToken && !(profileUpdate instanceof Error)) {
|
|
46
57
|
profileUpdate.refresh = encrypt(refreshToken)
|
|
58
|
+
}
|
|
47
59
|
|
|
48
|
-
if (
|
|
60
|
+
if ("email" in profileUpdate) {
|
|
49
61
|
// Look for an existing user
|
|
50
62
|
User.findOneAndUpdate(
|
|
51
63
|
{
|
|
@@ -55,8 +67,12 @@ export const verifyGoogleUser = (accessToken, refreshToken, profile, done) => {
|
|
|
55
67
|
profileUpdate,
|
|
56
68
|
{ upsert: true, new: true, setDefaultsOnInsert: true },
|
|
57
69
|
)
|
|
58
|
-
.then(user =>
|
|
59
|
-
|
|
70
|
+
.then((user) => {
|
|
71
|
+
done(null, addJWT(config)(user.toObject()))
|
|
72
|
+
})
|
|
73
|
+
.catch((err) => {
|
|
74
|
+
done(err, null)
|
|
75
|
+
})
|
|
60
76
|
} else {
|
|
61
77
|
done(profileUpdate, null)
|
|
62
78
|
}
|
|
@@ -72,7 +88,7 @@ export const verifyORCIDUser = (
|
|
|
72
88
|
const token = `${profile.orcid}:${profile.access_token}`
|
|
73
89
|
orcid
|
|
74
90
|
.getProfile(token)
|
|
75
|
-
.then(info => {
|
|
91
|
+
.then((info) => {
|
|
76
92
|
profile.info = info
|
|
77
93
|
profile.provider = PROVIDERS.ORCID
|
|
78
94
|
const profileUpdate = loadProfile(profile)
|
|
@@ -80,9 +96,9 @@ export const verifyORCIDUser = (
|
|
|
80
96
|
{ providerId: profile.orcid, provider: profile.provider },
|
|
81
97
|
profileUpdate,
|
|
82
98
|
{ upsert: true, new: true, setDefaultsOnInsert: true },
|
|
83
|
-
).then(user => done(null, addJWT(config)(user.toObject())))
|
|
99
|
+
).then((user) => done(null, addJWT(config)(user.toObject())))
|
|
84
100
|
})
|
|
85
|
-
.catch(err => done(err, null))
|
|
101
|
+
.catch((err) => done(err, null))
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
export const setupPassportAuth = () => {
|
|
@@ -93,13 +109,13 @@ export const setupPassportAuth = () => {
|
|
|
93
109
|
const jwtStrategy = new JwtStrategy(
|
|
94
110
|
{ secretOrKey: config.auth.jwt.secret, jwtFromRequest },
|
|
95
111
|
(jwt, done) => {
|
|
96
|
-
if (jwt.scopes?.includes(
|
|
112
|
+
if (jwt.scopes?.includes("dataset:indexing")) {
|
|
97
113
|
done(null, {
|
|
98
114
|
admin: false,
|
|
99
115
|
blocked: false,
|
|
100
116
|
indexer: true,
|
|
101
117
|
})
|
|
102
|
-
} else if (jwt.scopes?.includes(
|
|
118
|
+
} else if (jwt.scopes?.includes("dataset:reviewer")) {
|
|
103
119
|
done(null, {
|
|
104
120
|
admin: false,
|
|
105
121
|
blocked: false,
|
|
@@ -109,7 +125,7 @@ export const setupPassportAuth = () => {
|
|
|
109
125
|
} else {
|
|
110
126
|
// A user must already exist to use a JWT to auth a request
|
|
111
127
|
User.findOne({ id: jwt.sub, provider: jwt.provider })
|
|
112
|
-
.then(user => {
|
|
128
|
+
.then((user) => {
|
|
113
129
|
if (user) done(null, user.toObject())
|
|
114
130
|
else done(null, false)
|
|
115
131
|
})
|
|
@@ -119,7 +135,7 @@ export const setupPassportAuth = () => {
|
|
|
119
135
|
)
|
|
120
136
|
passport.use(jwtStrategy)
|
|
121
137
|
} else {
|
|
122
|
-
throw new Error(
|
|
138
|
+
throw new Error("JWT_SECRET must be configured to allow authentication.")
|
|
123
139
|
}
|
|
124
140
|
|
|
125
141
|
// Google first
|
|
@@ -129,7 +145,7 @@ export const setupPassportAuth = () => {
|
|
|
129
145
|
clientID: config.auth.google.clientID,
|
|
130
146
|
clientSecret: config.auth.google.clientSecret,
|
|
131
147
|
callbackURL: `${config.url + config.apiPrefix}auth/google/callback`,
|
|
132
|
-
userProfileURL:
|
|
148
|
+
userProfileURL: "https://www.googleapis.com/oauth2/v3/userinfo",
|
|
133
149
|
},
|
|
134
150
|
verifyGoogleUser,
|
|
135
151
|
)
|
|
@@ -141,9 +157,8 @@ export const setupPassportAuth = () => {
|
|
|
141
157
|
if (config.auth.orcid.clientID && config.auth.orcid.clientSecret) {
|
|
142
158
|
const orcidStrategy = new ORCIDStrategy(
|
|
143
159
|
{
|
|
144
|
-
sandbox:
|
|
145
|
-
config.auth.orcid.apiURI
|
|
146
|
-
config.auth.orcid.apiURI.includes('sandbox'),
|
|
160
|
+
sandbox: config.auth.orcid.apiURI &&
|
|
161
|
+
config.auth.orcid.apiURI.includes("sandbox"),
|
|
147
162
|
clientID: config.auth.orcid.clientID,
|
|
148
163
|
clientSecret: config.auth.orcid.clientSecret,
|
|
149
164
|
callbackURL: `${config.url + config.apiPrefix}auth/orcid/callback`,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/** Middleware to check for authorization states on top of authentication */
|
|
2
2
|
|
|
3
|
-
import Dataset from
|
|
4
|
-
import Permission from
|
|
5
|
-
import Comment from
|
|
6
|
-
import
|
|
7
|
-
import mongoose from 'mongoose'
|
|
3
|
+
import Dataset from "../../models/dataset"
|
|
4
|
+
import Permission from "../../models/permission"
|
|
5
|
+
import Comment from "../../models/comment"
|
|
6
|
+
import mongoose from "mongoose"
|
|
8
7
|
const ObjectID = mongoose.Schema.Types.ObjectId
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -17,7 +16,7 @@ export const authenticated = (req, res, next) => {
|
|
|
17
16
|
if (req.isAuthenticated()) {
|
|
18
17
|
return next()
|
|
19
18
|
} else {
|
|
20
|
-
return res.status(401).send({ error:
|
|
19
|
+
return res.status(401).send({ error: "Not logged in." })
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
22
|
|
|
@@ -46,10 +45,10 @@ export const superuser = (req, res, next) => {
|
|
|
46
45
|
if (req.user.admin) {
|
|
47
46
|
return next()
|
|
48
47
|
} else {
|
|
49
|
-
return res.status(401).send({ error:
|
|
48
|
+
return res.status(401).send({ error: "You do not have admin access." })
|
|
50
49
|
}
|
|
51
50
|
} else {
|
|
52
|
-
return res.status(401).send({ error:
|
|
51
|
+
return res.status(401).send({ error: "Not logged in." })
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
54
|
|
|
@@ -61,21 +60,19 @@ export const superuser = (req, res, next) => {
|
|
|
61
60
|
* the request object.
|
|
62
61
|
*/
|
|
63
62
|
export const datasetAccess = (req, res, next) => {
|
|
64
|
-
|
|
63
|
+
const datasetId = req.params.datasetId
|
|
65
64
|
? req.params.datasetId
|
|
66
65
|
: req.query.datasetId
|
|
67
66
|
|
|
68
|
-
datasetId = bidsId.decodeId(datasetId) // handle old dataset request methods that encode ids
|
|
69
|
-
|
|
70
67
|
// check to make sure that the dataset exists
|
|
71
68
|
return Dataset.findOne({ id: datasetId })
|
|
72
69
|
.exec()
|
|
73
|
-
.then(dataset => {
|
|
70
|
+
.then((dataset) => {
|
|
74
71
|
// if dataset does not exist, return 404 error
|
|
75
72
|
if (!dataset) {
|
|
76
73
|
return res
|
|
77
74
|
.status(404)
|
|
78
|
-
.send({ error:
|
|
75
|
+
.send({ error: "The dataset you have requested does not exist." })
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
// if there is no user option on the request,
|
|
@@ -92,19 +89,19 @@ export const datasetAccess = (req, res, next) => {
|
|
|
92
89
|
// find permissions information for this user & dataset
|
|
93
90
|
Permission.findOne({ datasetId: datasetId, userId: req.user.id })
|
|
94
91
|
.exec()
|
|
95
|
-
.then(permission => {
|
|
92
|
+
.then((permission) => {
|
|
96
93
|
if (permission) {
|
|
97
94
|
req.hasAccess = true
|
|
98
95
|
return next()
|
|
99
96
|
} else {
|
|
100
97
|
return res
|
|
101
98
|
.status(401)
|
|
102
|
-
.send({ error:
|
|
99
|
+
.send({ error: "You do not have access to this dataset." })
|
|
103
100
|
}
|
|
104
101
|
})
|
|
105
|
-
.catch(err => res.status(404).send(err))
|
|
102
|
+
.catch((err) => res.status(404).send(err))
|
|
106
103
|
})
|
|
107
|
-
.catch(err => res.status(404).send(err))
|
|
104
|
+
.catch((err) => res.status(404).send(err))
|
|
108
105
|
}
|
|
109
106
|
|
|
110
107
|
/**
|
|
@@ -122,14 +119,14 @@ export const commentAccess = (req, res, next) => {
|
|
|
122
119
|
_id: new ObjectID(commentId),
|
|
123
120
|
})
|
|
124
121
|
.exec()
|
|
125
|
-
.then(comment => {
|
|
122
|
+
.then((comment) => {
|
|
126
123
|
if (comment.user._id === req.user.id) {
|
|
127
124
|
return next()
|
|
128
125
|
} else {
|
|
129
126
|
return res
|
|
130
127
|
.status(401)
|
|
131
|
-
.send({ error:
|
|
128
|
+
.send({ error: "You do not have access to this comment." })
|
|
132
129
|
}
|
|
133
130
|
})
|
|
134
|
-
.catch(err => res.status(404).send(err))
|
|
131
|
+
.catch((err) => res.status(404).send(err))
|
|
135
132
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import crypto from
|
|
2
|
-
import config from
|
|
1
|
+
import crypto from "crypto"
|
|
2
|
+
import config from "../config"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Map dataset IDs to a normal distribution of backend workers
|
|
@@ -7,8 +7,8 @@ import config from '../config'
|
|
|
7
7
|
* @param {number} range Integer bound for offset from hash
|
|
8
8
|
*/
|
|
9
9
|
export function hashDatasetToRange(dataset, range) {
|
|
10
|
-
const hash = crypto.createHash(
|
|
11
|
-
const hexstring = hash.digest().toString(
|
|
10
|
+
const hash = crypto.createHash("sha1").update(dataset, "utf8")
|
|
11
|
+
const hexstring = hash.digest().toString("hex")
|
|
12
12
|
return parseInt(hexstring.substring(32, 40), 16) % range
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`DOI minting utils > template() > accepts expected arguments 1`] = `
|
|
4
|
+
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
|
|
5
|
+
<resource xmlns:xsi=\\"http://www.w3.org/2001/XMLSchema-instance\\" xmlns=\\"http://datacite.org/schema/kernel-4\\" xsi:schemaLocation=\\"http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4/metadata.xsd\\">
|
|
6
|
+
<identifier identifierType=\\"DOI\\">12345</identifier>
|
|
7
|
+
<creators>
|
|
8
|
+
<creator><creatorName>A. User</creatorName></creator><creator><creatorName>B. User</creatorName></creator>
|
|
9
|
+
</creators>
|
|
10
|
+
<titles>
|
|
11
|
+
<title xml:lang=\\"en-us\\">Test Dataset</title>
|
|
12
|
+
</titles>
|
|
13
|
+
<publisher>Openneuro</publisher>
|
|
14
|
+
<publicationYear>1999</publicationYear>
|
|
15
|
+
<resourceType resourceTypeGeneral=\\"Dataset\\">fMRI</resourceType>
|
|
16
|
+
</resource>"
|
|
17
|
+
`;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { vi } from "vitest"
|
|
2
|
+
import { formatBasicAuth, template } from "../index.js"
|
|
3
|
+
|
|
4
|
+
vi.mock("ioredis")
|
|
5
|
+
|
|
6
|
+
describe("DOI minting utils", () => {
|
|
7
|
+
describe("auth()", () => {
|
|
8
|
+
it("returns a base64 basic auth string", () => {
|
|
9
|
+
const doiConfig = { username: "test", password: "12345" }
|
|
10
|
+
expect(formatBasicAuth(doiConfig)).toBe("Basic dGVzdDoxMjM0NQ==")
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
describe("template()", () => {
|
|
14
|
+
it("accepts expected arguments", () => {
|
|
15
|
+
const context = {
|
|
16
|
+
doi: "12345",
|
|
17
|
+
creators: ["A. User", "B. User"],
|
|
18
|
+
title: "Test Dataset",
|
|
19
|
+
year: "1999",
|
|
20
|
+
resourceType: "fMRI",
|
|
21
|
+
}
|
|
22
|
+
expect(template(context)).toMatchSnapshot()
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { vi } from
|
|
2
|
-
import { normalizeDOI } from
|
|
1
|
+
import { vi } from "vitest"
|
|
2
|
+
import { normalizeDOI } from "../normalize"
|
|
3
3
|
|
|
4
|
-
vi.mock(
|
|
4
|
+
vi.mock("ioredis")
|
|
5
5
|
|
|
6
|
-
describe(
|
|
7
|
-
it(
|
|
8
|
-
expect(normalizeDOI(
|
|
6
|
+
describe("DOI normalize utility", () => {
|
|
7
|
+
it("returns null for non-DOI input", () => {
|
|
8
|
+
expect(normalizeDOI("Sphinx of black quartz, judge my vow.")).toBe(null)
|
|
9
9
|
})
|
|
10
|
-
it(
|
|
11
|
-
expect(normalizeDOI(
|
|
12
|
-
|
|
10
|
+
it("returns the raw DOI value from a raw input", () => {
|
|
11
|
+
expect(normalizeDOI("10.18112/openneuro.ds000001.v1.0.0")).toBe(
|
|
12
|
+
"10.18112/openneuro.ds000001.v1.0.0",
|
|
13
13
|
)
|
|
14
14
|
})
|
|
15
|
-
it(
|
|
16
|
-
expect(normalizeDOI(
|
|
17
|
-
|
|
15
|
+
it("returns the raw DOI value from a URI input", () => {
|
|
16
|
+
expect(normalizeDOI("doi:10.18112/openneuro.ds000001.v1.0.0")).toBe(
|
|
17
|
+
"10.18112/openneuro.ds000001.v1.0.0",
|
|
18
18
|
)
|
|
19
|
-
expect(normalizeDOI(
|
|
20
|
-
|
|
19
|
+
expect(normalizeDOI("DOI:10.18112/openneuro.ds000001.v1.0.0")).toBe(
|
|
20
|
+
"10.18112/openneuro.ds000001.v1.0.0",
|
|
21
21
|
)
|
|
22
22
|
})
|
|
23
|
-
it(
|
|
23
|
+
it("returns the raw DOI value from a URI input", () => {
|
|
24
24
|
expect(
|
|
25
|
-
normalizeDOI(
|
|
26
|
-
).toBe(
|
|
25
|
+
normalizeDOI("https://doi.org/10.18112/openneuro.ds000001.v1.0.0"),
|
|
26
|
+
).toBe("10.18112/openneuro.ds000001.v1.0.0")
|
|
27
27
|
expect(
|
|
28
|
-
normalizeDOI(
|
|
29
|
-
).toBe(
|
|
28
|
+
normalizeDOI("HTTPS://DOI.ORG/10.18112/openneuro.ds000001.v1.0.0"),
|
|
29
|
+
).toBe("10.18112/openneuro.ds000001.v1.0.0")
|
|
30
30
|
})
|
|
31
31
|
})
|