@openneuro/app 4.47.6 → 4.47.7
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/app",
|
|
3
|
-
"version": "4.47.
|
|
3
|
+
"version": "4.47.7",
|
|
4
4
|
"description": "React JS web frontend for the OpenNeuro platform.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "public/client.js",
|
|
@@ -79,5 +79,5 @@
|
|
|
79
79
|
"publishConfig": {
|
|
80
80
|
"access": "public"
|
|
81
81
|
},
|
|
82
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "af17c267fa700dff5b13d1b31aaf4f89c0853165"
|
|
83
83
|
}
|
package/src/client.jsx
CHANGED
|
@@ -15,11 +15,10 @@ import { relayStylePagination } from "@apollo/client/utilities"
|
|
|
15
15
|
// TODO - This should be a global SCSS?
|
|
16
16
|
import "./scripts/components/page/page.scss"
|
|
17
17
|
import cookies from "./scripts/utils/cookies.js"
|
|
18
|
-
import {
|
|
18
|
+
import { shouldRemoveToken } from "./scripts/authentication/profile"
|
|
19
19
|
|
|
20
|
-
// Clear expired JWT before the app mounts to avoid stale auth state
|
|
21
|
-
|
|
22
|
-
if (profile && !guardExpired(profile)) {
|
|
20
|
+
// Clear expired or corrupted JWT before the app mounts to avoid stale auth state
|
|
21
|
+
if (shouldRemoveToken(cookies.getAll())) {
|
|
23
22
|
cookies.remove("accessToken", { path: "/" })
|
|
24
23
|
}
|
|
25
24
|
|
|
@@ -1,23 +1,89 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getProfile,
|
|
3
|
+
guardExpired,
|
|
4
|
+
parseJwt,
|
|
5
|
+
shouldRemoveToken,
|
|
6
|
+
} from "../profile"
|
|
7
|
+
|
|
8
|
+
const header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
|
|
9
|
+
const dummySig = "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
|
|
2
10
|
|
|
3
11
|
const asciiToken =
|
|
4
|
-
|
|
12
|
+
`${header}.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.${dummySig}`
|
|
5
13
|
const utf8Token =
|
|
6
|
-
|
|
14
|
+
`${header}.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iuelnue1jOenkeWtpuiAhSIsImlhdCI6MTUxNjIzOTAyMn0.${dummySig}`
|
|
15
|
+
|
|
16
|
+
// Token with exp far in the future
|
|
17
|
+
const validToken =
|
|
18
|
+
`${header}.eyJzdWIiOiJ1c2VyMTIzIiwiYWRtaW4iOmZhbHNlLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6OTk5OTk5OTk5OX0.${dummySig}`
|
|
19
|
+
// Token with exp = 1 (long expired)
|
|
20
|
+
const expiredToken =
|
|
21
|
+
`${header}.eyJzdWIiOiJ1c2VyMTIzIiwiYWRtaW4iOmZhbHNlLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MX0.${dummySig}`
|
|
7
22
|
|
|
8
23
|
describe("authentication/profile", () => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
24
|
+
describe("parseJwt", () => {
|
|
25
|
+
it("decodes a JWT to Javascript object", () => {
|
|
26
|
+
expect(parseJwt(asciiToken)).toEqual({
|
|
27
|
+
sub: "1234567890",
|
|
28
|
+
name: "John Doe",
|
|
29
|
+
iat: 1516239022,
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
it("decodes a JWT with Unicode strings", () => {
|
|
33
|
+
expect(parseJwt(utf8Token)).toEqual({
|
|
34
|
+
sub: "1234567890",
|
|
35
|
+
name: "神経科学者",
|
|
36
|
+
iat: 1516239022,
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe("getProfile", () => {
|
|
42
|
+
it("returns null when no accessToken cookie exists", () => {
|
|
43
|
+
expect(getProfile({})).toBeNull()
|
|
44
|
+
})
|
|
45
|
+
it("returns null for a corrupted token", () => {
|
|
46
|
+
expect(getProfile({ accessToken: "not-a-jwt" })).toBeNull()
|
|
47
|
+
})
|
|
48
|
+
it("returns the decoded profile for a valid token", () => {
|
|
49
|
+
const profile = getProfile({ accessToken: validToken })
|
|
50
|
+
expect(profile).toMatchObject({
|
|
51
|
+
sub: "user123",
|
|
52
|
+
admin: false,
|
|
53
|
+
exp: 9999999999,
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe("guardExpired", () => {
|
|
59
|
+
it("returns true for an unexpired token", () => {
|
|
60
|
+
const profile = getProfile({ accessToken: validToken })
|
|
61
|
+
expect(guardExpired(profile)).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
it("returns false for an expired token", () => {
|
|
64
|
+
const profile = getProfile({ accessToken: expiredToken })
|
|
65
|
+
expect(guardExpired(profile)).toBe(false)
|
|
66
|
+
})
|
|
67
|
+
it("returns false for a null profile", () => {
|
|
68
|
+
expect(guardExpired(null)).toBe(false)
|
|
14
69
|
})
|
|
15
70
|
})
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
71
|
+
|
|
72
|
+
describe("shouldRemoveToken", () => {
|
|
73
|
+
it("removes nothing when no token is present", () => {
|
|
74
|
+
expect(shouldRemoveToken({})).toBe(false)
|
|
75
|
+
})
|
|
76
|
+
it("keeps a valid unexpired token", () => {
|
|
77
|
+
expect(shouldRemoveToken({ accessToken: validToken })).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
it("removes an expired token", () => {
|
|
80
|
+
expect(shouldRemoveToken({ accessToken: expiredToken })).toBe(true)
|
|
81
|
+
})
|
|
82
|
+
it("removes a corrupted token", () => {
|
|
83
|
+
expect(shouldRemoveToken({ accessToken: "garbage" })).toBe(true)
|
|
84
|
+
})
|
|
85
|
+
it("removes an empty string token", () => {
|
|
86
|
+
expect(shouldRemoveToken({ accessToken: "" })).toBe(false)
|
|
21
87
|
})
|
|
22
88
|
})
|
|
23
89
|
})
|
|
@@ -36,6 +36,15 @@ export const getUnexpiredProfile = (cookies) => {
|
|
|
36
36
|
if (guardExpired(profile)) return profile
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Check if the accessToken cookie should be removed due to expiration or corruption
|
|
41
|
+
*/
|
|
42
|
+
export function shouldRemoveToken(cookies): boolean {
|
|
43
|
+
const profile = getProfile(cookies)
|
|
44
|
+
const hasToken = !!cookies["accessToken"]
|
|
45
|
+
return hasToken && (!profile || !guardExpired(profile))
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
/**
|
|
40
49
|
* Test for an expired token
|
|
41
50
|
* @param {object} profile A profile returned by getProfile()
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useEffect } from "react"
|
|
2
1
|
import { gql, useQuery } from "@apollo/client"
|
|
3
2
|
import { useCookies } from "react-cookie"
|
|
4
3
|
import { getProfile } from "../authentication/profile"
|
|
@@ -212,7 +211,7 @@ export const ADVANCED_SEARCH_DATASETS_QUERY = gql`
|
|
|
212
211
|
|
|
213
212
|
// Reusable hook to fetch user data
|
|
214
213
|
export const useUser = (userId?: string) => {
|
|
215
|
-
const [cookies
|
|
214
|
+
const [cookies] = useCookies()
|
|
216
215
|
const profile = getProfile(cookies)
|
|
217
216
|
const profileSub = profile?.sub
|
|
218
217
|
|
|
@@ -231,13 +230,6 @@ export const useUser = (userId?: string) => {
|
|
|
231
230
|
Sentry.captureException(userError)
|
|
232
231
|
}
|
|
233
232
|
|
|
234
|
-
// Clear invalid session: cookie exists but query failed or returned no user
|
|
235
|
-
useEffect(() => {
|
|
236
|
-
if (!userLoading && profile && (userError || !userData?.user)) {
|
|
237
|
-
removeCookie("accessToken", { path: "/" })
|
|
238
|
-
}
|
|
239
|
-
}, [userLoading, profile, userError, userData?.user, removeCookie])
|
|
240
|
-
|
|
241
233
|
return {
|
|
242
234
|
user: userData?.user,
|
|
243
235
|
loading: userLoading,
|
|
@@ -62,7 +62,7 @@ export const UserMenu: React.FC<UserMenuProps> = ({ signOutAndRedirect }) => {
|
|
|
62
62
|
|
|
63
63
|
return (
|
|
64
64
|
<span className="user-menu-wrap">
|
|
65
|
-
{user.orcid && (
|
|
65
|
+
{user.orcid && user.id !== "reviewer" && (
|
|
66
66
|
<span className="notifications-link">
|
|
67
67
|
<Link to={`/user/${user.orcid}/notifications/unread`}>
|
|
68
68
|
<i className="fa fa-inbox">
|