From a0a795dbe19d640f80475a0e8401d4d2f22b1343 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 4 Dec 2025 14:16:26 +0100 Subject: [PATCH 01/16] refactor: Replace jsonwebtoken with jose for JWT verification --- package.json | 3 +-- src/utils/session.ts | 50 ++++++++++++++++++++++++++++---------------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index d196db46..d9680426 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "@testing-library/user-event": "^11.2.1", "@types/gtag.js": "^0.0.3", "@types/js-cookie": "^2.2.6", - "@types/jsonwebtoken": "^8.5.0", "@types/lodash": "^4.14.154", "@types/react-content-loader": "^4.0.0", "@types/testing-library__cypress": "^5.0.5", @@ -56,8 +55,8 @@ "html-react-parser": "^5.1.12", "iso-3166-1": "^2.1.1", "iso-639-1": "^3.1.3", + "jose": "^6.1.3", "js-yaml-loader": "^1.2.2", - "jsonwebtoken": "^8.5.1", "linkify-react": "^4.2.0", "linkifyjs": "^4.2.0", "maltipoo": "https://github.com/datacite/maltipoo#2.0.3", diff --git a/src/utils/session.ts b/src/utils/session.ts index 2c4b7580..ae3f24b7 100644 --- a/src/utils/session.ts +++ b/src/utils/session.ts @@ -1,5 +1,6 @@ +import { useState, useEffect } from 'react' import { Cookies } from 'react-cookie-consent' -import JsonWebToken from 'jsonwebtoken' +import { jwtVerify, importSPKI } from 'jose' import { JWT_KEY } from 'src/data/constants' export type User = { @@ -7,26 +8,39 @@ export type User = { name: string } | null -export const session = () => { - // RSA public key - if (!JWT_KEY) return null +export const useSession = () => { + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) - const sessionCookie = Cookies.getJSON('_datacite') - const token = sessionCookie?.authenticated?.access_token - if (!token) return null + useEffect(() => { + const fetchUser = async () => { + // RSA public key + if (!JWT_KEY) { + setLoading(false) + return + } - let user: any = null - function setUser(error: any, payload: any) { - if (error) { - console.log('JWT verification error: ' + error.message) - return - } + const sessionCookie = Cookies.getJSON('_datacite') + const token = sessionCookie?.authenticated?.access_token + if (!token) { + setLoading(false) + return + } - user = payload - } + try { + const publicKey = await importSPKI(JWT_KEY, 'RS256') + const { payload } = await jwtVerify(token, publicKey) + setUser(payload as User) + } catch (error: any) { + console.log('JWT verification error: ' + error.message) + setUser(null) + } finally { + setLoading(false) + } + } - // verify asymmetric token, using RSA with SHA-256 hash algorithm - JsonWebToken.verify(token, JWT_KEY, { algorithms: ['RS256'] }, setUser) + fetchUser() + }, []) - return user as User + return { user, loading } } From 4167dfb8c53f0c78d7e239e8a481f35f159aa324 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 4 Dec 2025 14:17:09 +0100 Subject: [PATCH 02/16] test: Add useSession behavior tests and update yarn.lock --- cypress/e2e/session.test.ts | 22 +++++++++ yarn.lock | 94 +++---------------------------------- 2 files changed, 28 insertions(+), 88 deletions(-) create mode 100644 cypress/e2e/session.test.ts diff --git a/cypress/e2e/session.test.ts b/cypress/e2e/session.test.ts new file mode 100644 index 00000000..c2694050 --- /dev/null +++ b/cypress/e2e/session.test.ts @@ -0,0 +1,22 @@ +describe('useSession behavior', () => { + beforeEach(() => { + cy.setCookie('_consent', 'true'); + }); + + it('shows logged in state with valid token', () => { + cy.setCookie('_datacite', Cypress.env('userCookie'), { log: false }); + cy.visit('/'); + cy.get('#sign-in').should('contain.text', 'Martin Fenner'); // Adjust to expected user name from env cookie + }); + + it('shows logged out state without token', () => { + cy.visit('/'); + cy.get('#sign-in').should('contain.text', 'Sign In'); + }); + + it('shows logged out state with invalid token', () => { + cy.setCookie('_datacite', '{"authenticated":{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.invalid.signature"}}', { log: false }); + cy.visit('/'); + cy.get('#sign-in').should('contain.text', 'Sign In'); + }); +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 4626bf6c..f8815590 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2047,13 +2047,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/jsonwebtoken@^8.5.0": - version "8.5.9" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz#2c064ecb0b3128d837d2764aa0b117b0ff6e4586" - integrity sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg== - dependencies: - "@types/node" "*" - "@types/lodash@^4.14.154": version "4.17.20" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.20.tgz#1ca77361d7363432d29f5e55950d9ec1e1c6ea93" @@ -3403,11 +3396,6 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== -buffer-equal-constant-time@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== - buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -4559,13 +4547,6 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - edge-runtime@2.5.9: version "2.5.9" resolved "https://registry.yarnpkg.com/edge-runtime/-/edge-runtime-2.5.9.tgz#9daeb329f0339b8377483f230789b3d68f45f1d9" @@ -6484,6 +6465,11 @@ joi@^17.3.0: "@sideway/formula" "^3.0.1" "@sideway/pinpoint" "^2.0.0" +jose@^6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/jose/-/jose-6.1.3.tgz#8453d7be88af7bb7d64a0481d6a35a0145ba3ea5" + integrity sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ== + js-cookie@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" @@ -6633,22 +6619,6 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== -jsonwebtoken@^8.5.1: - version "8.5.1" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^5.6.0" - jsprim@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d" @@ -6669,23 +6639,6 @@ jsprim@^2.0.2: object.assign "^4.1.4" object.values "^1.1.6" -jwa@^1.4.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9" - integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw== - dependencies: - buffer-equal-constant-time "^1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -6965,11 +6918,6 @@ lodash.identity@~2.4.1: resolved "https://registry.yarnpkg.com/lodash.identity/-/lodash.identity-2.4.1.tgz#6694cffa65fef931f7c31ce86c74597cf560f4f1" integrity sha512-VRYX+8XipeLjorag5bz3YBBRJ+5kj8hVBzfnaHgXPZAVTYowBdY5l0M5ZnOmlAMCOXBFabQtm7f5VqjMKEji0w== -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== - lodash.isarray@~2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-2.4.1.tgz#b52a326c1f62f6d7da73a31d5401df6ef44f0fa1" @@ -6977,26 +6925,11 @@ lodash.isarray@~2.4.1: dependencies: lodash._isnative "~2.4.1" -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== - lodash.isfunction@~2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-2.4.1.tgz#2cfd575c73e498ab57e319b77fa02adef13a94d1" integrity sha512-6XcAB3izeQxPOQQNAJbbdjXbvWEt2Pn9ezPrjr4CwoLwmqsLVbsiEXD19cmmt4mbzOCOCdHzOQiUivUOJLra7w== -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== - lodash.isobject@~2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-2.4.1.tgz#5a2e47fe69953f1ee631a7eba1fe64d2d06558f5" @@ -7004,16 +6937,6 @@ lodash.isobject@~2.4.1: dependencies: lodash._objecttypes "~2.4.1" -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== - lodash.keys@~2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-2.4.1.tgz#48dea46df8ff7632b10d706b8acb26591e2b3727" @@ -7038,7 +6961,7 @@ lodash.noop@~2.4.1: resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-2.4.1.tgz#4fb54f816652e5ae10e8f72f717a388c7326538a" integrity sha512-uNcV98/blRhInPUGQEnj9ekXXfG+q+rfoNSFZgl/eBfog9yBDW9gfUv2AHX/rAF7zZRlzWhbslGhbGQFZlCkZA== -lodash.once@^4.0.0, lodash.once@^4.1.1: +lodash.once@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== @@ -8477,11 +8400,6 @@ semver@7.3.5: dependencies: lru-cache "^6.0.0" -semver@^5.6.0: - version "5.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" - integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== - semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3, semver@^7.7.1, semver@^7.7.2: version "7.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" From 7763589527361c1f278992afed23aa4618210d32 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 4 Dec 2025 14:17:46 +0100 Subject: [PATCH 03/16] refactor: Fetch auth token before passing to providers --- src/app/(main)/layout.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 5bcf5cb8..4d52113e 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -42,6 +42,7 @@ const sourceSans3 = Source_Sans_3({ }) export default async function RootLayout({ children }: PropsWithChildren) { + const authToken = await getAuthToken() return ( @@ -54,7 +55,7 @@ export default async function RootLayout({ children }: PropsWithChildren) { - +
{children}
From d3389b60190129ac2b4a765b20272a5f20fcbed8 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 4 Dec 2025 14:19:34 +0100 Subject: [PATCH 04/16] refactor: Use useSession hook in multiple components --- src/components/Claim/Claim.tsx | 4 ++-- src/components/DiscoverWorksAlert/DiscoverWorksAlert.tsx | 4 ++-- src/components/Header/ClientButtons.tsx | 6 +++--- src/components/Header/Dropdown.tsx | 4 ++-- src/components/Header/NavRight.tsx | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/Claim/Claim.tsx b/src/components/Claim/Claim.tsx index 188f1396..c5b4526c 100644 --- a/src/components/Claim/Claim.tsx +++ b/src/components/Claim/Claim.tsx @@ -5,7 +5,7 @@ import { useEffect } from 'react'; import { useMutation, ApolloCache } from '@apollo/client' import { faOrcid } from '@fortawesome/free-brands-svg-icons' -import { session, User } from 'src/utils/session' +import { useSession, User } from 'src/utils/session' import { Claim as ClaimType } from 'src/data/types' import Error from 'src/components/Error/Error' import ClaimStatus from 'src/components/ClaimStatus/ClaimStatus' @@ -80,7 +80,7 @@ export default function Claim({ doi_id }: Props) { } }) - const user = session() + const { user } = useSession() const claim: ClaimType = data?.work.claims[0] || { id: null, diff --git a/src/components/DiscoverWorksAlert/DiscoverWorksAlert.tsx b/src/components/DiscoverWorksAlert/DiscoverWorksAlert.tsx index 4dcde86f..f886c707 100644 --- a/src/components/DiscoverWorksAlert/DiscoverWorksAlert.tsx +++ b/src/components/DiscoverWorksAlert/DiscoverWorksAlert.tsx @@ -9,12 +9,12 @@ import DataCiteButton from 'src/components/DataCiteButton/DataCiteButton' import { faSearch } from '@fortawesome/free-solid-svg-icons' import { PROFILES_SETTINGS_URL } from 'src/data/constants' -import { session } from "src/utils/session"; +import { useSession } from "src/utils/session"; import styles from './DiscoverWorksAlert.module.scss' export default function DiscoverWorksAlert() { - const user = session() + const { user } = useSession() const [show, setShow] = useState(true); diff --git a/src/components/Header/ClientButtons.tsx b/src/components/Header/ClientButtons.tsx index f4b8ac32..fa151b2b 100644 --- a/src/components/Header/ClientButtons.tsx +++ b/src/components/Header/ClientButtons.tsx @@ -7,10 +7,10 @@ import { faAddressCard } from '@fortawesome/free-solid-svg-icons' import { faOrcid } from '@fortawesome/free-brands-svg-icons' import { ORCID_URL } from 'src/data/constants'; -import { session } from "src/utils/session"; +import { useSession } from "src/utils/session"; export function UserCommonsPageButton() { - const user = session() + const { user } = useSession() if (!user) throw new Error("User not signed in") const href = '/orcid.org/' + user.uid @@ -21,7 +21,7 @@ export function UserCommonsPageButton() { export function UserOrcidButton() { - const user = session() + const { user } = useSession() if (!user) throw new Error("User not signed in") const href = `${ORCID_URL}/${user.uid}` diff --git a/src/components/Header/Dropdown.tsx b/src/components/Header/Dropdown.tsx index 2abbb489..7aaf4eaf 100644 --- a/src/components/Header/Dropdown.tsx +++ b/src/components/Header/Dropdown.tsx @@ -1,11 +1,11 @@ 'use client' import React, { PropsWithChildren } from 'react' -import { session } from 'src/utils/session' +import { useSession } from 'src/utils/session' import NavDropdown from 'react-bootstrap/NavDropdown' export default function UserDropdown({ children }: PropsWithChildren) { - const user = session() + const { user } = useSession() if (!user) throw new Error("User not signed in") return diff --git a/src/components/Header/NavRight.tsx b/src/components/Header/NavRight.tsx index f269b22e..782ff46b 100644 --- a/src/components/Header/NavRight.tsx +++ b/src/components/Header/NavRight.tsx @@ -1,7 +1,7 @@ 'use client' import React from 'react'; -import { session } from 'src/utils/session'; +import { useSession } from 'src/utils/session'; interface Props { signedInContent: React.ReactNode @@ -9,7 +9,7 @@ interface Props { } export default function NavRight({ signedInContent, signedOutContent }: Props) { - const user = session() + const { user } = useSession() if (!user) return signedOutContent return signedInContent From 6c505ed846d639c4aa7a9251f60bb50e7fae06d8 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 4 Dec 2025 14:20:07 +0100 Subject: [PATCH 05/16] fix: Make getAuthToken and setContext async --- src/utils/apolloClient/apolloClient.ts | 7 ++++--- src/utils/apolloClient/builder.ts | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/utils/apolloClient/apolloClient.ts b/src/utils/apolloClient/apolloClient.ts index f66be1c9..f2908d84 100644 --- a/src/utils/apolloClient/apolloClient.ts +++ b/src/utils/apolloClient/apolloClient.ts @@ -1,8 +1,9 @@ -import { cookies, type UnsafeUnwrappedCookies } from 'next/headers'; +import { cookies } from 'next/headers'; import apolloClientBuilder from './builder' -export function getAuthToken() { - const sessionCookie = JSON.parse(((cookies() as unknown as UnsafeUnwrappedCookies).get('_datacite') as any)?.value || '{}') +export async function getAuthToken() { + const cookieStore = await cookies() + const sessionCookie = JSON.parse((cookieStore.get('_datacite') as any)?.value || '{}') return sessionCookie?.authenticated?.access_token } diff --git a/src/utils/apolloClient/builder.ts b/src/utils/apolloClient/builder.ts index e21a5946..c87083ee 100644 --- a/src/utils/apolloClient/builder.ts +++ b/src/utils/apolloClient/builder.ts @@ -8,16 +8,16 @@ import { DATACITE_API_URL } from 'src/data/constants' * this throws an error unless the token is returned from a function that is called * in the authLink setContext. I'm not sure why */ -export default function apolloClientBuilder(getToken: () => string) { +export default function apolloClientBuilder(getToken: () => Promise) { // needed for CORS, see https://www.apollographql.com/docs/react/networking/authentication/#cookie const httpLink = createHttpLink({ uri: DATACITE_API_URL + '/graphql', credentials: 'include' }) - const authLink = setContext((_, { headers }) => { + const authLink = setContext(async (_, { headers }) => { // return the headers to the context so httpLink can read them - const token = getToken() + const token = await getToken() return { headers: { From f32043dc80290b6460a187729ee0f17e6c15b7dc Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 4 Dec 2025 14:20:22 +0100 Subject: [PATCH 06/16] refactor: Remove unused `on` and `config` from setupNodeEvents --- cypress.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress.config.ts b/cypress.config.ts index 72876e20..fec6fd49 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -6,12 +6,12 @@ export default defineConfig({ projectId: 'yur1cf', retries: 2, e2e: { - setupNodeEvents(on, config) {}, + setupNodeEvents() {}, baseUrl: 'http://localhost:3000', specPattern: 'cypress/e2e/**/*.test.*', }, component: { - setupNodeEvents(on, config) { }, + setupNodeEvents() { }, specPattern: 'src/components/**/*.test.*', devServer: { bundler: 'webpack', From 3070469605d347e31696091165875b47712e883e Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 4 Dec 2025 14:35:33 +0100 Subject: [PATCH 07/16] feat: Update ApolloProvider to accept nullable token and make token retrieval async --- src/utils/apolloClient/provider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/apolloClient/provider.tsx b/src/utils/apolloClient/provider.tsx index 309cf8a0..c6e32824 100644 --- a/src/utils/apolloClient/provider.tsx +++ b/src/utils/apolloClient/provider.tsx @@ -2,9 +2,9 @@ import React, { PropsWithChildren } from 'react' import apolloClientBuilder from 'src/utils/apolloClient/builder' import { ApolloNextAppProvider } from "@apollo/experimental-nextjs-app-support"; -export default function ApolloProvider({ token, children }: PropsWithChildren<{ token: string }>) { +export default function ApolloProvider({ token, children }: PropsWithChildren<{ token: string | null }>) { return ( - apolloClientBuilder(() => token)}> + apolloClientBuilder(async () => token)}> {children} ); From 3438bf8cf1865f2f6d74fda20270e395202e92f2 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 4 Dec 2025 17:14:19 +0100 Subject: [PATCH 08/16] test: add session diagnostic logging --- cypress/e2e/session.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cypress/e2e/session.test.ts b/cypress/e2e/session.test.ts index c2694050..d9097de0 100644 --- a/cypress/e2e/session.test.ts +++ b/cypress/e2e/session.test.ts @@ -4,6 +4,18 @@ describe('useSession behavior', () => { }); it('shows logged in state with valid token', () => { + cy.then(() => { + const userCookie = Cypress.env('userCookie'); + Cypress.log({ + name: 'diagnostics:userCookie', + message: [ + `type=${typeof userCookie}`, + `present=${Boolean(userCookie)}`, + `stringLength=${typeof userCookie === 'string' ? userCookie.length : 'n/a'}`, + ], + }); + }); + cy.setCookie('_datacite', Cypress.env('userCookie'), { log: false }); cy.visit('/'); cy.get('#sign-in').should('contain.text', 'Martin Fenner'); // Adjust to expected user name from env cookie From ff20ffd508ce757734b8ee8ec9b9a021baff339c Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 4 Dec 2025 17:35:21 +0100 Subject: [PATCH 09/16] test: Remove log option from cy.setCookie --- cypress/e2e/session.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/session.test.ts b/cypress/e2e/session.test.ts index d9097de0..72d658ee 100644 --- a/cypress/e2e/session.test.ts +++ b/cypress/e2e/session.test.ts @@ -16,7 +16,7 @@ describe('useSession behavior', () => { }); }); - cy.setCookie('_datacite', Cypress.env('userCookie'), { log: false }); + cy.setCookie('_datacite', Cypress.env('userCookie')); cy.visit('/'); cy.get('#sign-in').should('contain.text', 'Martin Fenner'); // Adjust to expected user name from env cookie }); From 839ab59300d6d9a472ad5ab7dfa85d0dd2fea997 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 4 Dec 2025 18:21:42 +0100 Subject: [PATCH 10/16] chore: Add CYPRESS_userCookie to cypress_tests.yml --- .github/workflows/cypress_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cypress_tests.yml b/.github/workflows/cypress_tests.yml index 52394586..07e0257b 100644 --- a/.github/workflows/cypress_tests.yml +++ b/.github/workflows/cypress_tests.yml @@ -43,4 +43,5 @@ jobs: SITEMAPS_URL: ${{ secrets.SITEMAPS_URL }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} CYPRESS_USER_COOKIE: ${{ secrets.CYPRESS_USER_COOKIE }} + CYPRESS_userCookie: ${{ secrets.CYPRESS_USER_COOKIE }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 2a19b55d32d157c58d353f8474e27cff167974f3 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 4 Dec 2025 18:38:08 +0100 Subject: [PATCH 11/16] test: improve session handling and test cookie logging --- cypress/e2e/session.test.ts | 16 ++-------------- src/components/Header/Dropdown.tsx | 3 ++- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/cypress/e2e/session.test.ts b/cypress/e2e/session.test.ts index 72d658ee..42454e4c 100644 --- a/cypress/e2e/session.test.ts +++ b/cypress/e2e/session.test.ts @@ -4,21 +4,9 @@ describe('useSession behavior', () => { }); it('shows logged in state with valid token', () => { - cy.then(() => { - const userCookie = Cypress.env('userCookie'); - Cypress.log({ - name: 'diagnostics:userCookie', - message: [ - `type=${typeof userCookie}`, - `present=${Boolean(userCookie)}`, - `stringLength=${typeof userCookie === 'string' ? userCookie.length : 'n/a'}`, - ], - }); - }); - - cy.setCookie('_datacite', Cypress.env('userCookie')); + cy.setCookie('_datacite', Cypress.env('userCookie'), { log: false }); cy.visit('/'); - cy.get('#sign-in').should('contain.text', 'Martin Fenner'); // Adjust to expected user name from env cookie + cy.get('#sign-in').should('contain.text', 'DataCite'); // Adjust to expected user name from env cookie }); it('shows logged out state without token', () => { diff --git a/src/components/Header/Dropdown.tsx b/src/components/Header/Dropdown.tsx index 7aaf4eaf..d0d643fa 100644 --- a/src/components/Header/Dropdown.tsx +++ b/src/components/Header/Dropdown.tsx @@ -5,7 +5,8 @@ import { useSession } from 'src/utils/session' import NavDropdown from 'react-bootstrap/NavDropdown' export default function UserDropdown({ children }: PropsWithChildren) { - const { user } = useSession() + const { user, loading } = useSession() + if (loading) return null // Or return a loading skeleton if (!user) throw new Error("User not signed in") return From 0df3a55aa6b2f61d0ff7c61fc8e08aeec9869abe Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 4 Dec 2025 19:04:28 +0100 Subject: [PATCH 12/16] feat: Add NEXT_PUBLIC_JWT_PUBLIC_KEY to cypress tests --- .github/workflows/cypress_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cypress_tests.yml b/.github/workflows/cypress_tests.yml index 07e0257b..8b63e206 100644 --- a/.github/workflows/cypress_tests.yml +++ b/.github/workflows/cypress_tests.yml @@ -40,6 +40,7 @@ jobs: CYPRESS_NODE_ENV: test NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} NEXT_PUBLIC_ORCID_API_URL: ${{ secrets.NEXT_PUBLIC_ORCID_API_URL }} + NEXT_PUBLIC_JWT_PUBLIC_KEY: ${{ secrets.JWT_PUBLIC_KEY}} SITEMAPS_URL: ${{ secrets.SITEMAPS_URL }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} CYPRESS_USER_COOKIE: ${{ secrets.CYPRESS_USER_COOKIE }} From 83055133e9b4f8c1d75f77055cd923ded9309d79 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Fri, 5 Dec 2025 11:26:56 +0100 Subject: [PATCH 13/16] refactor: Update Cypress environment variables --- .github/workflows/cypress_tests.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cypress_tests.yml b/.github/workflows/cypress_tests.yml index 8b63e206..349abae4 100644 --- a/.github/workflows/cypress_tests.yml +++ b/.github/workflows/cypress_tests.yml @@ -40,9 +40,8 @@ jobs: CYPRESS_NODE_ENV: test NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} NEXT_PUBLIC_ORCID_API_URL: ${{ secrets.NEXT_PUBLIC_ORCID_API_URL }} - NEXT_PUBLIC_JWT_PUBLIC_KEY: ${{ secrets.JWT_PUBLIC_KEY}} + NEXT_PUBLIC_JWT_PUBLIC_KEY: ${{ secrets.CYPRESS_JWT_PUBLIC_KEY_2025}} SITEMAPS_URL: ${{ secrets.SITEMAPS_URL }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - CYPRESS_USER_COOKIE: ${{ secrets.CYPRESS_USER_COOKIE }} - CYPRESS_userCookie: ${{ secrets.CYPRESS_USER_COOKIE }} + CYPRESS_userCookie: ${{ secrets.CYPRESS_USER_COOKIE_2025 }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From c7e67a22dc063ee7e73488cf0c7d33a25d22ff78 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Fri, 5 Dec 2025 11:30:28 +0100 Subject: [PATCH 14/16] build: Update Node.js version in Cypress tests --- .github/workflows/cypress_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cypress_tests.yml b/.github/workflows/cypress_tests.yml index 349abae4..56a85d3f 100644 --- a/.github/workflows/cypress_tests.yml +++ b/.github/workflows/cypress_tests.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - name: Checkout uses: actions/checkout@v4 - name: Cypress run From e854a69871b2be2929ce79535552f12f6b03ebee Mon Sep 17 00:00:00 2001 From: jrhoads Date: Fri, 5 Dec 2025 12:47:40 +0100 Subject: [PATCH 15/16] Add back in diagnostic messages --- cypress/e2e/session.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cypress/e2e/session.test.ts b/cypress/e2e/session.test.ts index 42454e4c..987fe808 100644 --- a/cypress/e2e/session.test.ts +++ b/cypress/e2e/session.test.ts @@ -4,6 +4,17 @@ describe('useSession behavior', () => { }); it('shows logged in state with valid token', () => { + cy.then(() => { + const userCookie = Cypress.env('userCookie'); + Cypress.log({ + name: 'diagnostics:userCookie', + message: [ + `type=${typeof userCookie}`, + `present=${Boolean(userCookie)}`, + `stringLength=${typeof userCookie === 'string' ? userCookie.length : 'n/a'}`, + ], + }); + }); cy.setCookie('_datacite', Cypress.env('userCookie'), { log: false }); cy.visit('/'); cy.get('#sign-in').should('contain.text', 'DataCite'); // Adjust to expected user name from env cookie From a02d475b0cfc177192f9f9107fd32e5a609ef118 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Fri, 5 Dec 2025 12:59:57 +0100 Subject: [PATCH 16/16] More diagnostics --- cypress/e2e/session.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/session.test.ts b/cypress/e2e/session.test.ts index 987fe808..821fde4a 100644 --- a/cypress/e2e/session.test.ts +++ b/cypress/e2e/session.test.ts @@ -5,19 +5,24 @@ describe('useSession behavior', () => { it('shows logged in state with valid token', () => { cy.then(() => { - const userCookie = Cypress.env('userCookie'); + let userCookie = Cypress.env('userCookie'); + if (typeof userCookie === 'object') { + userCookie = JSON.stringify(userCookie); + } Cypress.log({ name: 'diagnostics:userCookie', message: [ `type=${typeof userCookie}`, `present=${Boolean(userCookie)}`, `stringLength=${typeof userCookie === 'string' ? userCookie.length : 'n/a'}`, + `preview=${userCookie.substring(0, 50)}...`, ], }); }); - cy.setCookie('_datacite', Cypress.env('userCookie'), { log: false }); + + cy.setCookie('_datacite', String(Cypress.env('userCookie')), { log: false }); cy.visit('/'); - cy.get('#sign-in').should('contain.text', 'DataCite'); // Adjust to expected user name from env cookie + cy.get('#sign-in').should('contain.text', 'DataCite Test User'); // Match your JWT payload name }); it('shows logged out state without token', () => {