@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.6",
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": "e00e369a538900524544a396e7fd60e5e41d715e"
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 { getProfile, guardExpired } from "./scripts/authentication/profile"
18
+ import { shouldRemoveToken } from "./scripts/authentication/profile"
19
19
 
20
- // Clear expired JWT before the app mounts to avoid stale auth state
21
- const profile = getProfile(cookies.getAll())
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 { parseJwt } from "../profile"
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
- "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
12
+ `${header}.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.${dummySig}`
5
13
  const utf8Token =
6
- "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iuelnue1jOenkeWtpuiAhSIsImlhdCI6MTUxNjIzOTAyMn0.pUw2ARoXv4LkJXB1ZR3Th6xG83URT6mn1TftC7ac_O8"
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
- it("decodes a JWT to Javascript object", () => {
10
- expect(parseJwt(asciiToken)).toEqual({
11
- sub: "1234567890",
12
- name: "John Doe",
13
- iat: 1516239022,
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
- it("decodes a JWT with Unicode strings", () => {
17
- expect(parseJwt(utf8Token)).toEqual({
18
- sub: "1234567890",
19
- name: "神経科学者",
20
- iat: 1516239022,
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, , removeCookie] = useCookies()
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">