diff --git a/.env.example b/.env.example index ecf25e0..3b2116f 100644 --- a/.env.example +++ b/.env.example @@ -60,13 +60,13 @@ RSD_ENVIRONMENT=prod # Allowed values are: SURFCONEXT, ORCID, AZURE, LINKEDIN or LOCAL # if env value is not provided default provider is set to be SURFCONEXT # if you add the value "LOCAL", then local accounts are enabled, USE THIS FOR TESTING PURPOSES ONLY -RSD_AUTH_PROVIDERS=LINKEDIN;ORCID;LOCAL +RSD_AUTH_PROVIDERS=LINKEDIN:EVERYONE;ORCID:INVITE_ONLY # consumed by services: authentication, frontend (api/fe) # provide a list of supported OpenID auth providers for coupling with the user's RSD account # the values should be separated by semicolon (;) # Allowed values are: ORCID -RSD_AUTH_COUPLE_PROVIDERS=ORCID +RSD_AUTH_COUPLE_PROVIDERS=ORCID;LINKEDIN # Define a semicolon-separated list of user email addresses which are allowed to # login to the RSD. If the variable is left empty, or is not defined, all users diff --git a/data-generation/accounts.js b/data-generation/accounts.js index 640a3ff..c9d12d4 100644 --- a/data-generation/accounts.js +++ b/data-generation/accounts.js @@ -6,6 +6,7 @@ export async function generateAccounts(orcids){ const accounts = await postAccountsToBackend(100); const ids = accounts.map(a => a.id) const logins = await postToBackend('/login_for_account', generateLoginForAccount(ids, orcids)) + const profiles = await postToBackend('/user_profile', generateUserProfiles(ids)) // console.log('accounts, login_for_accounts done'); return ids } @@ -80,3 +81,28 @@ export function generateLoginForAccount(accountIds, orcids) { return login_for_accounts; } +function generateUserProfiles(accountIds) { + const user_profiles = accountIds.map(account => { + let given_names = faker.person.firstName(); + let family_names = faker.person.lastName(); + // user_profile table props + return { + account, + given_names, + family_names, + email_address: faker.internet.email({ + firstName: given_names, + lastName: family_names, + }), + role: faker.person.jobTitle(), + affiliation: faker.company.name(), + is_public: + faker.helpers.maybe(() => true, { + probability: 0.5, + }) ?? false, + avatar_id: null, + description: null, + }; + }); + return user_profiles; +} \ No newline at end of file diff --git a/deployment/.env.example b/deployment/.env.example index eea82c2..0345568 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -60,13 +60,13 @@ RSD_ENVIRONMENT=prod # Allowed values are: SURFCONEXT, ORCID, AZURE, LINKEDIN or LOCAL # if env value is not provided default provider is set to be SURFCONEXT # if you add the value "LOCAL", then local accounts are enabled, USE THIS FOR TESTING PURPOSES ONLY -RSD_AUTH_PROVIDERS=SURFCONEXT;LOCAL +RSD_AUTH_PROVIDERS=LINKEDIN:EVERYONE;ORCID:INVITE_ONLY # consumed by services: authentication, frontend (api/fe) # provide a list of supported OpenID auth providers for coupling with the user's RSD account # the values should be separated by semicolon (;) # Allowed values are: ORCID -# RSD_AUTH_COUPLE_PROVIDERS=ORCID +# RSD_AUTH_COUPLE_PROVIDERS=ORCID;LINKEDIN # Define a semicolon-separated list of user email addresses which are allowed to # login to the RSD. If the variable is left empty, or is not defined, all users @@ -198,8 +198,8 @@ CROSSREF_CONTACT_EMAIL= # consumed by: frontend # URL (should end with a trailing slash) and ID for Matomo Tracking Code -# MATOMO_URL= -# MATOMO_ID= +MATOMO_URL= +MATOMO_ID= # consumed by: scrapers # LIBRARIES_IO_ACCESS_TOKEN= diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index f20f862..dc5cf42 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -5,7 +5,7 @@ services: database: container_name: database - image: ghcr.io/research-software-directory/rsd-saas/database:v2.29.0 + image: ghcr.io/research-software-directory/rsd-saas/database:v3.3.0 expose: - 5432 environment: @@ -25,7 +25,7 @@ services: backend: container_name: backend - image: ghcr.io/research-software-directory/rsd-saas/backend:v2.29.0 + image: ghcr.io/research-software-directory/rsd-saas/backend:v3.3.0 expose: - 3500 environment: @@ -43,7 +43,7 @@ services: auth: container_name: auth - image: ghcr.io/research-software-directory/rsd-saas/auth:v2.29.0 + image: ghcr.io/research-software-directory/rsd-saas/auth:v3.3.0 expose: - 7000 environment: @@ -52,10 +52,13 @@ services: - POSTGREST_URL - RSD_AUTH_COUPLE_PROVIDERS - RSD_AUTH_PROVIDERS - - RSD_AUTH_USER_MAIL_WHITELIST - SURFCONEXT_CLIENT_ID - SURFCONEXT_REDIRECT - SURFCONEXT_WELL_KNOWN_URL + - HELMHOLTZID_CLIENT_ID + - HELMHOLTZID_REDIRECT + - HELMHOLTZID_WELL_KNOWN_URL + - HELMHOLTZID_SCOPES - ORCID_CLIENT_ID - ORCID_REDIRECT - ORCID_REDIRECT_COUPLE @@ -66,7 +69,9 @@ services: - AZURE_ORGANISATION - LINKEDIN_CLIENT_ID - LINKEDIN_REDIRECT + - LINKEDIN_REDIRECT_COUPLE - LINKEDIN_WELL_KNOWN_URL + - HELMHOLTZID_ALLOW_EXTERNAL_USERS - AUTH_SURFCONEXT_CLIENT_SECRET - AUTH_HELMHOLTZID_CLIENT_SECRET - AUTH_ORCID_CLIENT_SECRET @@ -102,16 +107,31 @@ services: - net restart: unless-stopped + background-services: + image: ghcr.io/research-software-directory/rsd-saas/background-services:v3.3.0 + environment: + # it uses values from .env file + - POSTGRES_DB_HOST + - POSTGRES_DB_HOST_PORT + - POSTGRES_DB + - POSTGRES_USER + - POSTGRES_PASSWORD + depends_on: + - database + networks: + - net + frontend: container_name: frontend image: ghcr.io/research-software-directory/kin-rpd/frontend:latest environment: - # it uses values from .env file + # it uses values from .env file - POSTGREST_URL - PGRST_JWT_SECRET - RSD_AUTH_URL - RSD_AUTH_PROVIDERS - RSD_AUTH_COUPLE_PROVIDERS + - RSD_REVERSE_PROXY_URL - MATOMO_URL - MATOMO_ID - SURFCONEXT_CLIENT_ID @@ -119,6 +139,11 @@ services: - SURFCONEXT_WELL_KNOWN_URL - SURFCONEXT_SCOPES - SURFCONEXT_RESPONSE_MODE + - HELMHOLTZID_CLIENT_ID + - HELMHOLTZID_REDIRECT + - HELMHOLTZID_WELL_KNOWN_URL + - HELMHOLTZID_SCOPES + - HELMHOLTZID_RESPONSE_MODE - ORCID_CLIENT_ID - ORCID_REDIRECT - ORCID_REDIRECT_COUPLE @@ -133,6 +158,7 @@ services: - AZURE_DESCRIPTION_HTML - LINKEDIN_CLIENT_ID - LINKEDIN_REDIRECT + - LINKEDIN_REDIRECT_COUPLE - LINKEDIN_WELL_KNOWN_URL - CROSSREF_CONTACT_EMAIL expose: diff --git a/docker-compose.yml b/docker-compose.yml index 0db8c69..80c226a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: database: - image: ghcr.io/research-software-directory/rsd-saas/database:v3.1.0 + image: ghcr.io/research-software-directory/rsd-saas/database:v3.3.0 ports: # enable connection from outside (development mode) - "5432:5432" @@ -23,7 +23,7 @@ services: - net backend: - image: ghcr.io/research-software-directory/rsd-saas/backend:v3.1.0 + image: ghcr.io/research-software-directory/rsd-saas/backend:v3.3.0 expose: - 3500 environment: @@ -39,7 +39,7 @@ services: - net auth: - image: ghcr.io/research-software-directory/rsd-saas/auth:v3.1.0 + image: ghcr.io/research-software-directory/rsd-saas/auth:v3.3.0 ports: - 5005:5005 expose: @@ -50,10 +50,13 @@ services: - POSTGREST_URL - RSD_AUTH_COUPLE_PROVIDERS - RSD_AUTH_PROVIDERS - - RSD_AUTH_USER_MAIL_WHITELIST - SURFCONEXT_CLIENT_ID - SURFCONEXT_REDIRECT - SURFCONEXT_WELL_KNOWN_URL + - HELMHOLTZID_CLIENT_ID + - HELMHOLTZID_REDIRECT + - HELMHOLTZID_WELL_KNOWN_URL + - HELMHOLTZID_SCOPES - ORCID_CLIENT_ID - ORCID_REDIRECT - ORCID_REDIRECT_COUPLE @@ -64,7 +67,9 @@ services: - AZURE_ORGANISATION - LINKEDIN_CLIENT_ID - LINKEDIN_REDIRECT + - LINKEDIN_REDIRECT_COUPLE - LINKEDIN_WELL_KNOWN_URL + - HELMHOLTZID_ALLOW_EXTERNAL_USERS - AUTH_SURFCONEXT_CLIENT_SECRET - AUTH_HELMHOLTZID_CLIENT_SECRET - AUTH_ORCID_CLIENT_SECRET @@ -85,7 +90,7 @@ services: ] scrapers: - image: ghcr.io/research-software-directory/rsd-saas/scrapers:v3.1.0 + image: ghcr.io/research-software-directory/rsd-saas/scrapers:v3.3.0 environment: # it uses values from .env file - POSTGREST_URL @@ -104,13 +109,27 @@ services: networks: - net + background-services: + image: ghcr.io/research-software-directory/rsd-saas/background-services:v3.3.0 + environment: + # it uses values from .env file + - POSTGRES_DB_HOST + - POSTGRES_DB_HOST_PORT + - POSTGRES_DB + - POSTGRES_USER + - POSTGRES_PASSWORD + depends_on: + - database + networks: + - net + frontend: build: context: ./frontend # dockerfile to use for build dockerfile: Dockerfile # update version number to correspond to frontend/package.json - image: kin-rpd/frontend:0.1.0 + image: kin-rpd/frontend:1.0.0 environment: # it uses values from .env file - POSTGREST_URL @@ -118,6 +137,7 @@ services: - RSD_AUTH_URL - RSD_AUTH_PROVIDERS - RSD_AUTH_COUPLE_PROVIDERS + - RSD_REVERSE_PROXY_URL - MATOMO_URL - MATOMO_ID - SURFCONEXT_CLIENT_ID @@ -125,6 +145,11 @@ services: - SURFCONEXT_WELL_KNOWN_URL - SURFCONEXT_SCOPES - SURFCONEXT_RESPONSE_MODE + - HELMHOLTZID_CLIENT_ID + - HELMHOLTZID_REDIRECT + - HELMHOLTZID_WELL_KNOWN_URL + - HELMHOLTZID_SCOPES + - HELMHOLTZID_RESPONSE_MODE - ORCID_CLIENT_ID - ORCID_REDIRECT - ORCID_REDIRECT_COUPLE @@ -139,6 +164,7 @@ services: - AZURE_DESCRIPTION_HTML - LINKEDIN_CLIENT_ID - LINKEDIN_REDIRECT + - LINKEDIN_REDIRECT_COUPLE - LINKEDIN_WELL_KNOWN_URL - CROSSREF_CONTACT_EMAIL expose: @@ -161,7 +187,7 @@ services: context: ./documentation # dockerfile to use for build dockerfile: Dockerfile - image: kin-rpd/documentation:0.1.0 + image: kin-rpd/documentation:1.0.0 expose: - "80" networks: @@ -170,7 +196,7 @@ services: nginx: build: context: ./nginx - image: kin-rpd/nginx:0.0.1 + image: kin-rpd/nginx:1.0.0 ports: - "80:80" - "443:443" @@ -191,7 +217,7 @@ services: #---------------------------------------------- data-generation: build: ./data-generation - image: kin-rpd/generation:0.0.1 + image: kin-rpd/generation:1.0.0 environment: # it needs to be here to use values from .env file - PGRST_JWT_SECRET diff --git a/frontend/.dockerignore b/frontend/.dockerignore index 8fd5ace..0344c7c 100644 --- a/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -1,5 +1,7 @@ # SPDX-FileCopyrightText: 2021 Dusan Mijatovic (dv4all) # SPDX-FileCopyrightText: 2021 dv4all +# SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +# SPDX-FileCopyrightText: 2024 Netherlands eScience Center # # SPDX-License-Identifier: Apache-2.0 @@ -15,6 +17,7 @@ # next.js /.next/ +/.swc/ /out/ # production diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json deleted file mode 100644 index 0d85e2c..0000000 --- a/frontend/.eslintrc.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "extends": "next/core-web-vitals", - "rules": { - // --- Linting --- - "no-debugger": "warn", - "no-console": "warn", - // use direct imports on material-ui to improve - // performance in unit tests with jest - // see https://blog.bitsrc.io/why-is-my-jest-suite-so-slow-2a4859bb9ac0 - "no-restricted-imports": [ - "warn", - { - "name": "@mui/material", - "message": "Please use \"import foo from '@mui/material/foo'\" instead." - } - ], - // do not warn for use of img element - "@next/next/no-img-element": "off", - // --- Formating --- - "eol-last": [ - "warn", - "always" - ], - "quotes": [ - "warn", - "single" - ], - "semi": [ - "warn", - "never" - ], - "indent": [ - "warn", - 2, - { - "SwitchCase": 1 - } - ], - "no-trailing-spaces": "warn", - "no-multi-spaces": [ - "warn" - ], - "no-multiple-empty-lines": "warn", - "object-curly-spacing": [ - "warn", - "never" - ], - "array-bracket-spacing": [ - "warn", - "never" - ] - } -} \ No newline at end of file diff --git a/frontend/.eslintrc.json.license b/frontend/.eslintrc.json.license deleted file mode 100644 index 906822e..0000000 --- a/frontend/.eslintrc.json.license +++ /dev/null @@ -1,7 +0,0 @@ -SPDX-FileCopyrightText: 2021 - 2023 Dusan Mijatovic (dv4all) -SPDX-FileCopyrightText: 2021 - 2023 dv4all -SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -SPDX-FileCopyrightText: 2023 Netherlands eScience Center - -SPDX-License-Identifier: Apache-2.0 -SPDX-License-Identifier: CC-BY-4.0 diff --git a/frontend/.knip.jsonc b/frontend/.knip.jsonc new file mode 100644 index 0000000..8dd96ac --- /dev/null +++ b/frontend/.knip.jsonc @@ -0,0 +1,25 @@ +{ + "ignore":[ + // ignore mocks as some are not imported/used yet but can be in the future + "**/__mocks__/**", + // ignore jest setup file + "jest.setup.js", + // these are "required" in next.config.js but knip does not see that :-( + "next.headers.js", + "next.rewrites.js" + ], + // see https://knip.dev/reference/configuration#ignoreexportsusedinfile + "ignoreExportsUsedInFile": { + "interface": true, + "type": true + }, + // see https://knip.dev/reference/configuration#includeentryexports + "includeEntryExports": true, + // see https://knip.dev/guides/handling-issues#unreachable-code + "ignoreDependencies": [ + // ignore tailwind plugin + "@tailwindcss/typography", + // ignore sharp used in next standalone mode + "sharp" + ] +} \ No newline at end of file diff --git a/frontend/.unimportedrc.json b/frontend/.unimportedrc.json deleted file mode 100644 index ec4eedd..0000000 --- a/frontend/.unimportedrc.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "ignorePatterns": [ - "**/node_modules/**", - "**/*.tests.{js,jsx,ts,tsx}", - "**/*.test.{js,jsx,ts,tsx}", - "**/*.spec.{js,jsx,ts,tsx}", - "**/tests/**", - "**/__tests__/**", - "**/*.d.ts", - "**/coverage/**", - "**/__mocks__/**", - "utils/jest/**" - ], - "ignoreUnimported": [ - "jest.config.js", - "jest.setup.js", - "next.config.js", - "next.headers.js", - "next.rewrites.js", - "postcss.config.js", - "tailwind.config.js" - ], - "ignoreUnused": [ - "next", - "react", - "react-dom", - "@tailwindcss/typography", - "sharp" - ], - "ignoreUnresolved": [ - "pages/api/fe/auth" - ] -} \ No newline at end of file diff --git a/frontend/.unimportedrc.json.license b/frontend/.unimportedrc.json.license deleted file mode 100644 index 546d6c9..0000000 --- a/frontend/.unimportedrc.json.license +++ /dev/null @@ -1,4 +0,0 @@ -SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -SPDX-FileCopyrightText: 2023 Netherlands eScience Center - -SPDX-License-Identifier: Apache-2.0 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 94c8a74..819eb98 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,7 +1,8 @@ # SPDX-FileCopyrightText: 2021 - 2022 Dusan Mijatovic (dv4all) # SPDX-FileCopyrightText: 2021 - 2022 dv4all -# SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -# SPDX-FileCopyrightText: 2023 Netherlands eScience Center +# SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +# SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center +# SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) # # SPDX-License-Identifier: Apache-2.0 @@ -12,14 +13,14 @@ # ---------------------------------------- # 1. Install dependencies only when needed # ---------------------------------------- -FROM node:20.15-slim AS deps +FROM node:22.13.1-bookworm-slim AS deps WORKDIR /app # copy COPY package.json package-lock.json ./ # install -RUN npm install --frozen-lockfile --silent +RUN npm ci --silent # ---------------------------------------- # 2. Build image @@ -39,7 +40,7 @@ COPY . . ENV NEXT_TELEMETRY_DISABLED 1 # unit tests are already runned on PR and push to main -# RUN npm test +# RUN npm run test # build the solution RUN npm run build @@ -47,7 +48,7 @@ RUN npm run build # ---------------------------------------- # 3. Production image (standalone mode) # ---------------------------------------- -FROM node:20.15-slim AS runner +FROM node:22.13.1-bookworm-slim AS runner # optional install updates # RUN apt-get upgrade -y @@ -75,4 +76,4 @@ ENV PORT 3000 # set next hostname ENV HOSTNAME "0.0.0.0" -CMD ["node", "server.js"] \ No newline at end of file +CMD ["node", "server.js"] diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev index f4155fd..9a3b00e 100644 --- a/frontend/Dockerfile.dev +++ b/frontend/Dockerfile.dev @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: Apache-2.0 -FROM node:20.15-slim +FROM node:22.13.1-bookworm-slim # Change the user and group id of the node user to the specified ids ARG DUID=1000 @@ -27,4 +27,4 @@ VOLUME [ "/app" ] EXPOSE 3000 -CMD [ "sh", "-c", "yarn install ; yarn dev:docker" ] +CMD [ "sh", "-c", "npm install ; npm run dev:docker" ] diff --git a/frontend/__tests__/CookiesPage.test.tsx b/frontend/__tests__/CookiesPage.test.tsx index db2892e..183381f 100644 --- a/frontend/__tests__/CookiesPage.test.tsx +++ b/frontend/__tests__/CookiesPage.test.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -13,6 +13,9 @@ import CookiesPage from '../pages/cookies' // use DEFAULT MOCK for login providers list // required when AppHeader component is used jest.mock('~/auth/api/useLoginProviders') +// MOCK global search +jest.mock('~/components/GlobalSearchAutocomplete/apiGlobalSearch') +jest.mock('~/components/GlobalSearchAutocomplete/useHasRemotes') it('renders cookies page with title Cookies', async() => { @@ -22,8 +25,8 @@ it('renders cookies page with title Cookies', async() => { } render(WrappedComponentWithProps( CookiesPage, { - props - })) + props + })) const heading = await screen.findByRole('heading',{ name: 'Cookies' }) @@ -39,8 +42,8 @@ it('renders cookies page with anonymous statistics checkbox ON', async() => { } render(WrappedComponentWithProps( CookiesPage, { - props - })) + props + })) const heading = await screen.findByRole('heading',{ name: 'Tracking cookies' @@ -61,8 +64,8 @@ it('renders cookies page with anonymous statistics checkbox OFF', async() => { } render(WrappedComponentWithProps( CookiesPage, { - props - })) + props + })) const checkbox = await screen.findByRole('checkbox', { checked:false diff --git a/frontend/__tests__/OrganisationPage.test.tsx b/frontend/__tests__/OrganisationPage.test.tsx index d8d0812..aba6465 100644 --- a/frontend/__tests__/OrganisationPage.test.tsx +++ b/frontend/__tests__/OrganisationPage.test.tsx @@ -1,8 +1,9 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center +// SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) // // SPDX-License-Identifier: Apache-2.0 @@ -18,22 +19,33 @@ import mockSoftware from '~/components/organisation/software/__mocks__/mockSoftw import mockProjects from '~/components/organisation/projects/__mocks__/mockProjects.json' import mockUnits from '~/components/organisation/units/__mocks__/mockUnits.json' import {TabKey} from '~/components/organisation/tabs/OrganisationTabItems' +import {OrganisationForContext} from '~/components/organisation/context/OrganisationContext' -// MOCK user agreement call -jest.mock('~/components/user/settings/useUserAgreements') +// mock user agreement call +jest.mock('~/components/user/settings/agreements/useUserAgreements') +// global search +jest.mock('~/components/GlobalSearchAutocomplete/apiGlobalSearch') +jest.mock('~/components/GlobalSearchAutocomplete/useHasRemotes') // use DEFAULT MOCK for login providers list // required when AppHeader component is used jest.mock('~/auth/api/useLoginProviders') +// mock project categories api +jest.mock('~/components/organisation/projects/filters/useOrgProjectCategoriesList') +// mock software categories api +jest.mock('~/components/organisation/software/filters/useOrgSoftwareCategoriesList') const mockProps = { - organisation: mockOrganisation, + organisation: mockOrganisation as OrganisationForContext, slug:['dutch-research-council'], - tab: 'software' as TabKey, + tab: 'projects' as TabKey, ror: mockRORIinfo as any, isMaintainer: false, rsd_page_rows: 12, - rsd_page_layout: 'grid' + rsd_page_layout: 'grid', + units: mockUnits, + releaseCountsByYear: [], + releases: [] } as OrganisationPageProps // MOCK isMaintainerOfOrganisation @@ -105,8 +117,7 @@ describe('pages/organisations/[...slug].tsx', () => { // screen.debug(aboutPage) }) - // disable software option, 2024-07-02 - // it('renders organisation software page as default when organisation.description=null', async () => { + // it('renders organisation project page as default when organisation.description=null', async () => { // // not a maintainer - public page // mockIsMaintainerOfOrganisation.mockResolvedValueOnce(false) // // when no about page content, software is default landing page @@ -127,10 +138,10 @@ describe('pages/organisations/[...slug].tsx', () => { // // wait loader to be removed // // await waitForElementToBeRemoved(screen.getByRole('progressbar')) // // we need to await for all events to run - // const software = await screen.findAllByTestId('software-grid-card') + // const software = await screen.findAllByTestId('project-grid-card') // expect(software.length).toEqual(mockSoftware.length) // // validate api call - TODO! FIGURE WHY IS CALLED TWICE!!! - // expect(mockSoftwareForOrganisation).toBeCalledTimes(1) + // expect(mockSoftwareForOrganisation).toHaveBeenCalledTimes(1) // }) it('renders organisation projects page when page=projects', async () => { @@ -158,7 +169,7 @@ describe('pages/organisations/[...slug].tsx', () => { const cards = await screen.findAllByTestId('project-grid-card') expect(cards.length).toEqual(mockProjects.length) // validate api call - TODO! FIGURE WHY IS CALLED TWICE!!! - expect(mockProjectsForOrganisation).toBeCalledTimes(1) + expect(mockProjectsForOrganisation).toHaveBeenCalledTimes(1) }) it('shows organisation units', async() => { @@ -182,7 +193,7 @@ describe('pages/organisations/[...slug].tsx', () => { await waitForElementToBeRemoved(screen.getByRole('progressbar')) // validate api call - expect(mockGetOrganisationChildren).toBeCalledTimes(1) + expect(mockGetOrganisationChildren).toHaveBeenCalledTimes(0) // validate units const units = screen.getAllByTestId('research-unit-item') expect(units.length).toEqual(mockUnits.length) diff --git a/frontend/__tests__/ProjectEditPage.test.tsx b/frontend/__tests__/ProjectEditPage.test.tsx index 397d155..5a9b81f 100644 --- a/frontend/__tests__/ProjectEditPage.test.tsx +++ b/frontend/__tests__/ProjectEditPage.test.tsx @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 dv4all // // SPDX-License-Identifier: Apache-2.0 @@ -21,7 +21,10 @@ import mockProjectToEdit from '~/components/projects/edit/information/__mocks__/ // we mock default providers used in page header jest.mock('~/auth/api/useLoginProviders') // mock user agreement call -jest.mock('~/components/user/settings/useUserAgreements') +jest.mock('~/components/user/settings/agreements/useUserAgreements') +// global search +jest.mock('~/components/GlobalSearchAutocomplete/apiGlobalSearch') +jest.mock('~/components/GlobalSearchAutocomplete/useHasRemotes') // MOCK isMaintainerOf const mockIsMaintainer = jest.fn(props => Promise.resolve(false)) @@ -44,7 +47,6 @@ window.IntersectionObserver = jest.fn(() => ({ unobserve: mockUnobserve, } as any)) - const mockProps = { // information page pageIndex: 0, diff --git a/frontend/__tests__/ProjectItem.test.tsx b/frontend/__tests__/ProjectItem.test.tsx index 047dc42..9167e30 100644 --- a/frontend/__tests__/ProjectItem.test.tsx +++ b/frontend/__tests__/ProjectItem.test.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -40,7 +40,11 @@ const mockedProps: ProjectPageProps = { impact: apiMentions as MentionItemProps[], team: apiContributors, relatedProjects: apiRelatedProjects as RelatedProject[], - relatedSoftware: apiRelatedSoftware as any + relatedSoftware: apiRelatedSoftware as any, + testimonials:[], + categories:[], + orgMaintainer:[], + comMaintainer:[] } describe('pages/projects/[slug]/index.tsx', () => { diff --git a/frontend/__tests__/ProjectsOverviewPage.test.tsx b/frontend/__tests__/ProjectsOverviewPage.test.tsx index d11e7d5..e9a2e6c 100644 --- a/frontend/__tests__/ProjectsOverviewPage.test.tsx +++ b/frontend/__tests__/ProjectsOverviewPage.test.tsx @@ -1,16 +1,14 @@ -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 dv4all // // SPDX-License-Identifier: Apache-2.0 import {render, screen, within} from '@testing-library/react' -import {WithAppContext} from '~/utils/jest/WithAppContext' +import {WithAppContext, defaultUserSettings} from '~/utils/jest/WithAppContext' import ProjectOverviewPage from '../pages/projects/index' -import {LayoutType} from '~/components/software/overview/search/ViewToggleGroup' - import mockData from './__mocks__/projectsOverview.json' // use DEFAULT MOCK for login providers list @@ -27,7 +25,6 @@ const mockProps = { page: 1, rows: 12, count: 408, - layout: 'masonry' as LayoutType, keywordsList: mockData.keywordsList, domainsList: mockData.domainsList, organisationsList: mockData.organisationsList, @@ -35,7 +32,6 @@ const mockProps = { projects: mockData.projects as any } - describe('pages/projects/index.tsx', () => { beforeEach(() => { @@ -44,7 +40,7 @@ describe('pages/projects/index.tsx', () => { it('renders title All projects', () => { render( - + ) @@ -56,24 +52,24 @@ describe('pages/projects/index.tsx', () => { it('renders project filter panel with orderBy, project status and 3 other filters (combobox)', () => { render( - + ) // get reference to filter panel const panel = screen.getByTestId('filters-panel') // find order by testid - const order = within(panel).getByTestId('filters-order-by') - const status = within(panel).getByTestId('filters-project-status') - // should have 3 filters + // within(panel).getByTestId('filters-order-by') + // within(panel).getByTestId('filters-project-status') + // should have 5 filters const filters = within(panel).getAllByRole('combobox') - expect(filters.length).toEqual(3) + expect(filters.length).toEqual(5) // screen.debug(filters) }) it('renders searchbox with placeholder Find project', async () => { render( - + ) @@ -81,9 +77,8 @@ describe('pages/projects/index.tsx', () => { }) it('renders layout options (toggle button group)', async () => { - mockProps.layout='masonry' render( - + ) @@ -91,9 +86,9 @@ describe('pages/projects/index.tsx', () => { }) it('renders (12) grid cards (even for masonry layout type)', async () => { - mockProps.layout='masonry' + defaultUserSettings.rsd_page_layout='masonry' render( - + ) @@ -102,9 +97,9 @@ describe('pages/projects/index.tsx', () => { }) it('renders (12) list items', async () => { - mockProps.layout='list' + defaultUserSettings.rsd_page_layout='list' render( - + ) diff --git a/frontend/__tests__/SoftwareEditPage.test.tsx b/frontend/__tests__/SoftwareEditPage.test.tsx index 7465482..573337e 100644 --- a/frontend/__tests__/SoftwareEditPage.test.tsx +++ b/frontend/__tests__/SoftwareEditPage.test.tsx @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) // SPDX-FileCopyrightText: 2023 dv4all @@ -23,7 +23,10 @@ import {RsdSettingsState} from '~/config/rsdSettingsReducer' // we mock default providers used in page header jest.mock('~/auth/api/useLoginProviders') // mock user agreement call -jest.mock('~/components/user/settings/useUserAgreements') +jest.mock('~/components/user/settings/agreements/useUserAgreements') +// MOCK global search +jest.mock('~/components/GlobalSearchAutocomplete/apiGlobalSearch') +jest.mock('~/components/GlobalSearchAutocomplete/useHasRemotes') // MOCK isMaintainerOf const mockIsMaintainer = jest.fn(props => Promise.resolve(false)) diff --git a/frontend/__tests__/SoftwareItem.test.tsx b/frontend/__tests__/SoftwareItem.test.tsx deleted file mode 100644 index bab10f6..0000000 --- a/frontend/__tests__/SoftwareItem.test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 dv4all -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center -// -// SPDX-License-Identifier: Apache-2.0 - -import {render, screen} from '@testing-library/react' -import SoftwareItemPage from '../pages/software/[slug]/index' -import {WrappedComponentWithProps} from '../utils/jest/WrappedComponents' - -// mock fetch response -import softwareIndexData from './__mocks__/softwareIndexData' - -// use DEFAULT MOCK for login providers list -// required when AppHeader component is used -jest.mock('~/auth/api/useLoginProviders') - -jest.mock('../utils/getSoftware') - -// mock next router -const mockBack = jest.fn() -const mockReplace = jest.fn() -const mockPush = jest.fn() -jest.mock('next/router', () => ({ - useRouter: () => ({ - back: mockBack, - replace: mockReplace, - push: mockPush - }) -})) - -describe('pages/software/[slug]/index.tsx', () => { - it('renders heading with software title', async() => { - render(WrappedComponentWithProps( - SoftwareItemPage, - {props: softwareIndexData} - )) - const title = await screen.findByText(softwareIndexData.software.brand_name) - expect(title).toBeInTheDocument() - // screen.debug() - }) - it('renders edit button when isMaintainer=true', () => { - // set isMaintainer to true - softwareIndexData.isMaintainer=true - render(WrappedComponentWithProps( - SoftwareItemPage, - {props: softwareIndexData} - )) - const editBtn = screen.getByTestId('edit-button') - expect(editBtn).toBeInTheDocument() - // screen.debug(editBtn) - }) - it('DOES NOT render edit button when isMaintainer=false', () => { - // set isMaintainer to true - softwareIndexData.isMaintainer=false - render(WrappedComponentWithProps( - SoftwareItemPage, - {props: softwareIndexData} - )) - const editBtn = screen.queryByTestId('edit-button') - expect(editBtn).not.toBeInTheDocument() - // screen.debug(editBtn) - }) - it('render mentions count', () => { - render(WrappedComponentWithProps( - SoftwareItemPage, - {props: softwareIndexData} - )) - const expected = softwareIndexData.mentions.length - const mentions = screen.getByText(expected) - expect(mentions).toBeInTheDocument() - // screen.debug(editBtn) - }) - it('render contributor_cnt count', () => { - render(WrappedComponentWithProps( - SoftwareItemPage, - {props: softwareIndexData} - )) - const expected = softwareIndexData.contributors.length - const contributor_cnt = screen.getByText(expected) - expect(contributor_cnt).toBeInTheDocument() - // screen.debug(editBtn) - }) -}) diff --git a/frontend/__tests__/SoftwareOverview.test.tsx b/frontend/__tests__/SoftwareOverview.test.tsx index 51af8f5..e0053e2 100644 --- a/frontend/__tests__/SoftwareOverview.test.tsx +++ b/frontend/__tests__/SoftwareOverview.test.tsx @@ -1,15 +1,14 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 import {render, screen, within} from '@testing-library/react' -import {WithAppContext} from '~/utils/jest/WithAppContext' +import {WithAppContext,defaultUserSettings} from '~/utils/jest/WithAppContext' import SoftwareOverviewPage from '../pages/software/index' -import {LayoutType} from '~/components/software/overview/search/ViewToggleGroup' // use DEFAULT MOCK for login providers list // required when AppHeader component is used @@ -23,16 +22,19 @@ const mockProps = { keywords:null, prog_lang: null, licenses:null, - order:null, + sources: null, + order: '', page: 1, rows: 12, count: 408, - layout: 'masonry' as LayoutType, keywordsList: mockData.keywordsList, languagesList: mockData.languagesList, licensesList: mockData.licensesList, + sourcesList: [], + hostsList: [], software: mockData.software as any, - highlights: mockData.highlights as any + highlights: mockData.highlights as any, + hasRemotes: false, } describe('pages/software/index.tsx', () => { @@ -57,7 +59,7 @@ describe('pages/software/index.tsx', () => { ) - const carousel = screen.getByTestId('highlights-carousel') + screen.getByTestId('highlights-carousel') const cards = screen.getAllByTestId('highlights-card') expect(cards.length).toEqual(mockData.highlights.length) }) @@ -71,10 +73,10 @@ describe('pages/software/index.tsx', () => { // get reference to filter panel const panel = screen.getByTestId('filters-panel') // find order by testid - const order = within(panel).getByTestId('filters-order-by') - // should have 3 filters + // within(panel).getByTestId('filters-order-by') + // should have 4 filters const filters = within(panel).getAllByRole('combobox') - expect(filters.length).toEqual(3) + expect(filters.length).toEqual(4) // screen.debug(filters) }) @@ -88,19 +90,19 @@ describe('pages/software/index.tsx', () => { }) it('renders layout options (toggle button group)', async () => { - mockProps.layout='masonry' + defaultUserSettings.rsd_page_layout='masonry' render( - + ) - const buttonGroup = screen.getByTestId('card-layout-options') + screen.getByTestId('card-layout-options') }) it('renders (12) masonry cards', async () => { - mockProps.layout='masonry' + defaultUserSettings.rsd_page_layout='masonry' render( - + ) @@ -109,9 +111,9 @@ describe('pages/software/index.tsx', () => { }) it('renders (12) grid cards', async () => { - mockProps.layout='grid' + defaultUserSettings.rsd_page_layout='grid' render( - + ) @@ -120,9 +122,9 @@ describe('pages/software/index.tsx', () => { }) it('renders (12) list items', async () => { - mockProps.layout='list' + defaultUserSettings.rsd_page_layout='list' render( - + ) diff --git a/frontend/__tests__/UserPages.test.tsx b/frontend/__tests__/UserPages.test.tsx index 4aaef4d..e428c72 100644 --- a/frontend/__tests__/UserPages.test.tsx +++ b/frontend/__tests__/UserPages.test.tsx @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) // SPDX-FileCopyrightText: 2023 Ewan Cahen (Netherlands eScience Center) @@ -8,7 +8,7 @@ // SPDX-License-Identifier: Apache-2.0 import {render, screen} from '@testing-library/react' -import {WithAppContext, mockSession} from '~/utils/jest/WithAppContext' +import {WithAppContext, mockSession, defaultUserSettings} from '~/utils/jest/WithAppContext' import UserPages from '../pages/user/[section]' @@ -16,34 +16,52 @@ import mockSoftwareByMaintainer from '~/components/user/software/__mocks__/softw import mockProjectsByMaintainer from '~/components/user/project/__mocks__/projectsByMaintainer.json' import mockOrganisationsByMaintainer from '~/components/user/organisations/__mocks__/organisationsByMaintainer.json' import mockCommunitiesByMaintainer from '~/components/user/communities/__mocks__/communitiesByMaintainer.json' +import loginForAccount from '~/components/user/settings/profile/__mocks__/logins.json' +import {UserPageId} from '~/components/user/tabs/UserTabItems' +import {getDisplayName} from '~/utils/getDisplayName' +import { LoginForAccount } from '~/components/user/settings/profile/apiLoginForAccount' +// import {UserPageId} from '~/components/user/UserNavItems' // use DEFAULT MOCK for login providers list // required when AppHeader component is used jest.mock('~/auth/api/useLoginProviders') -// MOCK user agreement call -jest.mock('~/components/user/settings/useUserAgreements') +// mock user agreement call +jest.mock('~/components/user/settings/agreements/useUserAgreements') // MOCK user logins call -jest.mock('~/components/user/settings/useLoginForAccount') +jest.mock('~/components/user/settings/profile/useLoginForUser') // MOCK user project list jest.mock('~/components/user/project/useUserProjects') // MOCK user software list jest.mock('~/components/user/software/useUserSoftware') -// MOCK user software list -jest.mock('~/components/user/software/useUserSoftware') // MOCK user organisation list jest.mock('~/components/user/organisations/useUserOrganisations') // MOCK user communities list jest.mock('~/components/user/communities/useUserCommunities') +// MOCK global search +jest.mock('~/components/GlobalSearchAutocomplete/apiGlobalSearch') +jest.mock('~/components/GlobalSearchAutocomplete/useHasRemotes') // MOCKS const mockProps = { - section: 'software', + section: 'software' as UserPageId, counts: { software_cnt: 0, project_cnt: 0, organisation_cnt: 0, community_cnt: 0 }, - orcidAuthLink:null + orcidAuthLink:null, + linkedInAuthLink: null, + profile:{ + account: 'test-account-id', + given_names: 'Test given names', + family_names: 'Test family names', + email_address: null, + role: null, + affiliation: null, + is_public: false, + avatar_id: null + }, + logins:loginForAccount as LoginForAccount[] } describe('pages/user/[section].tsx', () => { @@ -64,52 +82,87 @@ describe('pages/user/[section].tsx', () => { expect(p401).toBeInTheDocument() }) - it('renders user nav items', () => { + it('renders user name', () => { mockProps.section = 'software' + const userName = getDisplayName(mockProps.profile) ?? 'No name' render( ) - const navItems = screen.getAllByTestId('user-nav-item') - // KIN-RPD has 4 items - expect(navItems.length).toEqual(4) + screen.getByText(RegExp(userName)) + // expect(navItems.length).toEqual(5) }) - it('renders user settings section', async() => { + it('renders user settings with User profile heading', async() => { mockProps.section = 'settings' render( ) - // settings page - const settings = await screen.findByTestId('user-settings-section') + await screen.findByTestId('user-profile-page-title') + // await screen.findByRole('heading',{name:'User profile'}) // shows user account if (mockSession.user?.account) { - const userId = screen.getByText(RegExp(mockSession.user?.account)) + screen.getByText(RegExp(mockSession.user?.account)) } }) - it('renders user software section', async() => { + it('renders user software list items', async() => { mockProps.section = 'software' + defaultUserSettings.rsd_page_layout = 'list' + render( + + + + ) + + // validate software cards are shown + const software = await screen.findAllByTestId('software-list-item') + expect(software.length).toEqual(mockSoftwareByMaintainer.length) + }) + it('renders user software grid items', async() => { + mockProps.section = 'software' + defaultUserSettings.rsd_page_layout = 'grid' render( - + ) // validate software cards are shown - const software = screen.getAllByTestId('software-list-item') + const software = await screen.findAllByTestId('software-grid-card') expect(software.length).toEqual(mockSoftwareByMaintainer.length) }) - it('renders user projects section', async() => { + + it('renders user projects list items', async() => { mockProps.section = 'projects' + defaultUserSettings.rsd_page_layout = 'list' + render( + + + + ) + + // validate project cards are shown + const project = await screen.findAllByTestId('project-list-item') + expect(project.length).toEqual(mockProjectsByMaintainer.length) + }) + it('renders user projects grid items', async() => { + mockProps.section = 'projects' + defaultUserSettings.rsd_page_layout = 'grid' render( @@ -117,35 +170,79 @@ describe('pages/user/[section].tsx', () => { ) // validate project cards are shown - const project = screen.getAllByTestId('project-list-item') + const project = await screen.findAllByTestId('project-grid-card') expect(project.length).toEqual(mockProjectsByMaintainer.length) }) - it('renders user organisation section', async() => { + it('renders user organisations list items', async() => { mockProps.section = 'organisations' + defaultUserSettings.rsd_page_layout = 'list' render( - + ) // validate project cards are shown - const project = screen.getAllByTestId('organisation-list-item') + const project = await screen.findAllByTestId('organisation-list-item') expect(project.length).toEqual(mockOrganisationsByMaintainer.length) }) - it('renders user communities section', async() => { - mockProps.section = 'communities' + it('renders user organisations grid items', async() => { + mockProps.section = 'organisations' + defaultUserSettings.rsd_page_layout = 'grid' render( - + ) // validate project cards are shown - const project = screen.getAllByTestId('community-list-item') + const project = await screen.findAllByTestId('organisation-card-link') + expect(project.length).toEqual(mockOrganisationsByMaintainer.length) + }) + + it('renders user communities list items', async() => { + mockProps.section = 'communities' + defaultUserSettings.rsd_page_layout = 'list' + + render( + + + + ) + + // validate community cards are shown + const project = await screen.findAllByTestId('community-list-item') + expect(project.length).toEqual(mockCommunitiesByMaintainer.length) + }) + + it('renders user communities card items', async() => { + mockProps.section = 'communities' + defaultUserSettings.rsd_page_layout = 'grid' + + render( + + + + ) + + // validate community cards are shown + const project = await screen.findAllByTestId('community-card-link') expect(project.length).toEqual(mockCommunitiesByMaintainer.length) }) diff --git a/frontend/__tests__/__mocks__/apiContributors.json b/frontend/__tests__/__mocks__/apiContributors.json index 4055cf3..42b2d29 100644 --- a/frontend/__tests__/__mocks__/apiContributors.json +++ b/frontend/__tests__/__mocks__/apiContributors.json @@ -10,7 +10,8 @@ "role": null, "orcid": null, "avatar_id": null, - "position": null + "position": null, + "account": null }, { "id": "c86ef6f6-5428-4142-b338-d8479611e5e6", @@ -23,7 +24,8 @@ "role": null, "orcid": null, "avatar_id": null, - "position": null + "position": null, + "account": null }, { "id": "27716d66-89f7-4f8f-b9a6-2a66ecf4762c", @@ -36,7 +38,8 @@ "role": null, "orcid": null, "avatar_id": null, - "position": null + "position": null, + "account": null }, { "id": "0a68cf00-649f-40b7-b469-d41760194844", @@ -49,7 +52,8 @@ "role": null, "orcid": null, "avatar_id": null, - "position": null + "position": null, + "account": null }, { "id": "d7f2c176-d188-49a4-8265-97a4e7456b47", @@ -62,7 +66,8 @@ "role": null, "orcid": null, "avatar_id": null, - "position": null + "position": null, + "account": null }, { "id": "0e9e77bd-f64a-4bb1-a4c9-362e77bb0495", @@ -75,7 +80,8 @@ "role": null, "orcid": null, "avatar_id": null, - "position": null + "position": null, + "account": null }, { "id": "6ed89f4b-62a9-41f1-811f-b88ed06da04c", @@ -88,7 +94,8 @@ "role": null, "orcid": null, "avatar_id": null, - "position": null + "position": null, + "account": null }, { "id": "d5e86fc3-79d8-47a1-83da-64ca0d0d8789", @@ -101,7 +108,8 @@ "role": null, "orcid": null, "avatar_id": null, - "position": null + "position": null, + "account": null }, { "id": "92df99ae-1945-4a8f-a8f2-6b94b492ee30", @@ -114,7 +122,8 @@ "role": null, "orcid": null, "avatar_id": null, - "position": null + "position": null, + "account": null }, { "id": "a7498680-2b2f-42d7-ac02-ba173a711c06", @@ -127,7 +136,8 @@ "role": null, "orcid": null, "avatar_id": null, - "position": null + "position": null, + "account": null }, { "id": "a07e253d-4edd-44d3-9d0c-3d25215bbf6c", @@ -140,7 +150,8 @@ "role": null, "orcid": null, "avatar_id": null, - "position": null + "position": null, + "account": null }, { "id": "21a54d99-aa96-4fe1-a84e-473c1f20a7a5", @@ -153,6 +164,7 @@ "role": null, "orcid": null, "avatar_id": null, - "position": null + "position": null, + "account": null } ] \ No newline at end of file diff --git a/frontend/auth/ProtectedContent.test.tsx b/frontend/auth/ProtectedContent.test.tsx index 893b36d..1b3a329 100644 --- a/frontend/auth/ProtectedContent.test.tsx +++ b/frontend/auth/ProtectedContent.test.tsx @@ -1,5 +1,7 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -10,7 +12,9 @@ import {WrappedComponentWithProps} from '../utils/jest/WrappedComponents' import ProtectedContent from './ProtectedContent' // MOCKS +// eslint-disable-next-line @typescript-eslint/no-unused-vars const softwareMaintainer=jest.fn((props)=>Promise.resolve(false)) +// eslint-disable-next-line @typescript-eslint/no-unused-vars const projectMaintainer=jest.fn((props)=>Promise.resolve(false)) jest.mock('./permissions/isMaintainerOfSoftware', () => { // console.log('mocked isMaintainerOfSoftware') @@ -139,9 +143,9 @@ it('renders content of type software when maintainer', async () => { expect(content).toBeInTheDocument() // ensure isMaintainerOfSoftware is called - expect(softwareMaintainer).toBeCalledTimes(1) + expect(softwareMaintainer).toHaveBeenCalledTimes(1) // with expected agrguments - expect(softwareMaintainer).toBeCalledWith({ + expect(softwareMaintainer).toHaveBeenCalledWith({ 'account': 'test-account-string', 'slug': '/test-software-1', 'token': 'TEST_RANDOM_TOKEN', @@ -174,9 +178,9 @@ it('renders content of type project when maintainer', async () => { expect(content).toBeInTheDocument() // ensure isMaintainerOfProject is called - expect(projectMaintainer).toBeCalledTimes(1) + expect(projectMaintainer).toHaveBeenCalledTimes(1) // with expected agrguments - expect(projectMaintainer).toBeCalledWith({ + expect(projectMaintainer).toHaveBeenCalledWith({ 'account': 'test-account-string', 'slug': '/test-project-1', 'token': 'TEST_RANDOM_TOKEN', diff --git a/frontend/auth/api/authEndpoint.ts b/frontend/auth/api/authEndpoint.ts index 7c3ca52..d9e9620 100644 --- a/frontend/auth/api/authEndpoint.ts +++ b/frontend/auth/api/authEndpoint.ts @@ -7,7 +7,7 @@ import logger from '~/utils/logger' import {getAuthorisationEndpoint} from './authHelpers' -type providers = 'surfconext'|'helmholtzid'|'orcid'|'azure'|'linkedin' +export type ProviderName = 'surfconext'|'helmholtz'|'orcid'|'azure'|'linkedin'|'local' // how often we refresh auth endpoint const refreshInterval = 60*60*1000 // save timer as public variable @@ -25,7 +25,7 @@ const cache:{ * refreshInterval defined how often we refresh auth endpoint info. * */ -export async function getAuthEndpoint(wellknownUrl:string,provider:providers){ +export async function getAuthEndpoint(wellknownUrl:string,provider:ProviderName){ try{ // if already present return existing value if (cache?.[provider]?.authEndpoint) { diff --git a/frontend/auth/api/authHelpers.test.ts b/frontend/auth/api/authHelpers.test.ts index 1a777fa..9d34539 100644 --- a/frontend/auth/api/authHelpers.test.ts +++ b/frontend/auth/api/authHelpers.test.ts @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center +// SPDX-FileCopyrightText: 2025 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2025 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // // SPDX-License-Identifier: Apache-2.0 @@ -45,6 +47,7 @@ const mockProps: RedirectToProps = { client_id: '1234567', scope: 'openid', response_mode: 'form', + prompt: 'login' } // mock console log @@ -63,12 +66,19 @@ beforeEach(() => { it('crates RedirectUrl', () => { - const {authorization_endpoint, redirect_uri, client_id, scope, response_mode} = mockProps - const expectedRedirect = `${authorization_endpoint}?redirect_uri=${redirect_uri}&client_id=${client_id}&scope=${scope}&response_type=code&response_mode=${response_mode}&prompt=login+consent` + const {authorization_endpoint, redirect_uri, client_id, scope, response_mode, prompt} = mockProps + const expectedRedirect = `${authorization_endpoint}?redirect_uri=${redirect_uri}&client_id=${client_id}&scope=${scope}&response_type=code&response_mode=${response_mode}&prompt=${prompt}` const redirectUrl = getRedirectUrl(mockProps) expect(redirectUrl).toEqual(expectedRedirect) }) +it('does not enforce prompt parameter', () => { + const {authorization_endpoint, redirect_uri, client_id, scope, response_mode} = mockProps + const expectedRedirect = `${authorization_endpoint}?redirect_uri=${redirect_uri}&client_id=${client_id}&scope=${scope}&response_type=code&response_mode=${response_mode}` + const redirectUrl = getRedirectUrl({...mockProps, prompt: undefined}) + expect(redirectUrl).toEqual(expectedRedirect) +}) + it('returns authorization_endpoint', async() => { mockResolvedValueOnce(mockWellKnowResp) const authorization_endpoint = await getAuthorisationEndpoint('mockedWellKnowEndpoint') @@ -133,15 +143,15 @@ it('claimProjectMaintainerInvite calls expected endpoint', async () => { }) // claim maintainer invite - const response = await claimProjectMaintainerInvite({ + await claimProjectMaintainerInvite({ id: expectedId, token: 'TEST_TOKEN', frontend: true }) // validate api call - expect(global.fetch).toBeCalledTimes(1) - expect(global.fetch).toBeCalledWith(expectedUrl,{ + expect(global.fetch).toHaveBeenCalledTimes(1) + expect(global.fetch).toHaveBeenCalledWith(expectedUrl,{ body: `{"invitation":"${expectedId}"}`, headers: { 'Accept': 'application/vnd.pgrst.object + json', @@ -211,15 +221,15 @@ it('claimSoftwareMaintainerInvite calls expected endpoint', async () => { }) // claim maintainer invite - const response = await claimSoftwareMaintainerInvite({ + await claimSoftwareMaintainerInvite({ id: expectedId, token: 'TEST_TOKEN', frontend: true }) // validate api call - expect(global.fetch).toBeCalledTimes(1) - expect(global.fetch).toBeCalledWith(expectedUrl, { + expect(global.fetch).toHaveBeenCalledTimes(1) + expect(global.fetch).toHaveBeenCalledWith(expectedUrl, { body: `{"invitation":"${expectedId}"}`, headers: { 'Accept': 'application/vnd.pgrst.object + json', @@ -289,15 +299,15 @@ it('claimOrganisationMaintainerInvite calls expected endpoint', async () => { }) // claim maintainer invite - const response = await claimOrganisationMaintainerInvite({ + await claimOrganisationMaintainerInvite({ id: expectedId, token: 'TEST_TOKEN', frontend: true }) // validate api call - expect(global.fetch).toBeCalledTimes(1) - expect(global.fetch).toBeCalledWith(expectedUrl, { + expect(global.fetch).toHaveBeenCalledTimes(1) + expect(global.fetch).toHaveBeenCalledWith(expectedUrl, { body: `{"invitation":"${expectedId}"}`, headers: { 'Accept': 'application/vnd.pgrst.object + json', diff --git a/frontend/auth/api/authHelpers.ts b/frontend/auth/api/authHelpers.ts index ac03ff3..bbb8bb1 100644 --- a/frontend/auth/api/authHelpers.ts +++ b/frontend/auth/api/authHelpers.ts @@ -3,6 +3,8 @@ // SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center // SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2025 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // // SPDX-License-Identifier: Apache-2.0 @@ -42,8 +44,11 @@ export function getRedirectUrl(props: RedirectToProps) { '&client_id=' + props.client_id + '&scope=' + props.scope + '&response_type=code' + - '&response_mode=' + props.response_mode + - '&prompt=' + (props.prompt ? props.prompt : 'login+consent') + '&response_mode=' + props.response_mode + + if (props?.prompt) { + redirectUrl += '&prompt=' + props.prompt + } if (props?.claims){ redirectUrl += '&claims=' + encodeURIComponent(JSON.stringify(props.claims)) @@ -190,7 +195,7 @@ export async function claimCommunityMaintainerInvite({id, token}: { id: string, token?: string}) { try { const query = 'rpc/accept_invitation_community' - let url = `${getBaseUrl()}/${query}` + const url = `${getBaseUrl()}/${query}` const resp = await fetch(url, { method: 'POST', diff --git a/frontend/auth/api/getLoginProviders.tsx b/frontend/auth/api/getLoginProviders.tsx new file mode 100644 index 0000000..a1a32e2 --- /dev/null +++ b/frontend/auth/api/getLoginProviders.tsx @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {Provider} from 'pages/api/fe/auth' +import logger from '~/utils/logger' + +// save info after initial call +let loginProviders:Provider[] = [] + +export async function getLoginProviders(baseUrl?:string) { + try{ + // console.group('getLoginProviders') + // console.log('baseUrl...', baseUrl) + // console.log('loginProviders...', loginProviders) + // console.groupEnd() + + if (loginProviders.length === 0){ + // baseUrl is required for requests from server side + // this is internal url of frontend, by default this + // is http://localhost:3000 + const url = `${baseUrl ?? ''}/api/fe/auth` + // console.log('url...', url) + const resp = await fetch(url) + + if (resp.status === 200){ + const providers: Provider[] = await resp.json() + // api response is the same once the app is started + // because the info eventually comes from .env file + // to avoid additional api calls we save api response + // into the loginProviders variable and reuse it + loginProviders = [ + ...providers + ] + }else{ + logger(`getLoginProviders: ${resp.status}: ${resp.statusText}`, 'warn') + } + } + return loginProviders + }catch(e:any){ + logger(`getLoginProviders: ${e.message}`, 'error') + return loginProviders + } +} diff --git a/frontend/auth/api/useLoginProviders.tsx b/frontend/auth/api/useLoginProviders.tsx index f1c4c8b..cf330f0 100644 --- a/frontend/auth/api/useLoginProviders.tsx +++ b/frontend/auth/api/useLoginProviders.tsx @@ -1,51 +1,31 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 import {useEffect, useState} from 'react' import {Provider} from 'pages/api/fe/auth' - -// save info after initial call -let loginProviders:Provider[] = [] +import {getLoginProviders} from './getLoginProviders' export default function useLoginProviders() { const [providers, setProviders] = useState([]) // console.group('useLoginProviders') // console.log('providers...', providers) - // console.log('loginProviders...', loginProviders) // console.groupEnd() useEffect(() => { let abort = false - async function getProviders() { - if (loginProviders.length === 0){ - const url = '/api/fe/auth' - const resp = await fetch(url) - if (resp.status === 200 && abort === false) { - const providers: Provider[] = await resp.json() - if (abort) return - setProviders(providers) - // api response is the same once the app is started - // because the info eventually comes from .env file - // to avoid additional api calls we save api response - // into the loginProviders variable and reuse it - loginProviders = [ - ...providers - ] - } - }else if (abort===false){ - setProviders(loginProviders) - } - } - if (abort === false) { - getProviders() - } + getLoginProviders() + .then(providers=>{ + if (abort) return + setProviders(providers) + }) + .catch(()=>setProviders([])) return () => { abort = true } }, []) diff --git a/frontend/auth/index.tsx b/frontend/auth/index.tsx index 4d03dec..e73e733 100644 --- a/frontend/auth/index.tsx +++ b/frontend/auth/index.tsx @@ -1,15 +1,17 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2024 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // // SPDX-License-Identifier: Apache-2.0 import {createContext, Dispatch, SetStateAction, useState, useContext, useEffect} from 'react' -import verifyJwt, {decodeJwt} from './jwtUtils' import {IncomingMessage, OutgoingMessage} from 'http' -import cookie from 'cookie' -import logger from '../utils/logger' +import {parse} from 'cookie' +import logger from '~/utils/logger' +import verifyJwt, {decodeJwt} from './jwtUtils' import {refreshSession} from './refreshSession' // refresh schedule margin 5min. before expiration time @@ -17,6 +19,9 @@ import {refreshSession} from './refreshSession' const testMargin = process.env.REFRESH_MARGIN_MSEC ? parseInt(process.env.REFRESH_MARGIN_MSEC) : undefined export const REFRESH_MARGIN = testMargin ?? 5 * 60 * 1000 export type RsdRole = 'rsd_admin' | 'rsd_user' +export type RsdUserData = { + [property: string]: string[] +} export type RsdUser = { iss: 'rsd_auth' role: RsdRole @@ -25,7 +30,8 @@ export type RsdUser = { // uid account: string // display name - name: string + name: string, + data?: RsdUserData, } export type Session = { @@ -129,7 +135,7 @@ export function useSession(){ } /** - * Calculate expirition time from now in milliseconds + * Calculate expiration time from now in milliseconds * @param exp in seconds * @returns difference in milliseconds */ @@ -171,7 +177,7 @@ export function getSessionSeverSide(req: IncomingMessage|undefined, res: Outgoin // get token from cookie const token = getRsdTokenNode(req) // create session from token - const session = createSession(token) + const session = createSession(token ?? null) // remove invalid cookie if (session.status === 'invalid') { // console.log('remove rsd cookies...') @@ -193,7 +199,7 @@ export function getRsdTokenNode(req: IncomingMessage){ // check for cookies if (req?.headers?.cookie) { // parse cookies from node request - const cookies = cookie.parse(req.headers.cookie) + const cookies = parse(req.headers.cookie) // validate and decode const token = cookies?.rsd_token return token diff --git a/frontend/auth/permissions/isMaintainerOfCommunity.ts b/frontend/auth/permissions/isMaintainerOfCommunity.ts index 8dda42f..e84f63c 100644 --- a/frontend/auth/permissions/isMaintainerOfCommunity.ts +++ b/frontend/auth/permissions/isMaintainerOfCommunity.ts @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -63,13 +63,13 @@ export async function isMaintainerOfCommunity({community, account, token}: isCom } export async function getCommunitiesOfMaintainer({token}: - {token: string}) { + {token?: string}) { try { // without token api request is not needed if (!token) return [] // build url const query = 'rpc/communities_of_current_maintainer' - let url = `${getBaseUrl()}/${query}` + const url = `${getBaseUrl()}/${query}` const resp = await fetch(url, { method: 'GET', headers: createJsonHeaders(token) diff --git a/frontend/auth/permissions/isMaintainerOfOrganisation.test.ts b/frontend/auth/permissions/isMaintainerOfOrganisation.test.ts index 04ee77a..03fdcb1 100644 --- a/frontend/auth/permissions/isMaintainerOfOrganisation.test.ts +++ b/frontend/auth/permissions/isMaintainerOfOrganisation.test.ts @@ -1,5 +1,7 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -35,19 +37,11 @@ it('returns true when organisation in list', async () => { expect(isMaintainer).toBe(true) // validate call - expect(global.fetch).toBeCalledTimes(1) - expect(global.fetch).toBeCalledWith(expectedUrl, expectedOptions) + expect(global.fetch).toHaveBeenCalledTimes(1) + expect(global.fetch).toHaveBeenCalledWith(expectedUrl, expectedOptions) }) it('returns false when organisation NOT in list', async () => { - const expectedUrl = '/api/v1/rpc/organisations_of_current_maintainer' - const expectedOptions = { - 'headers': { - 'Authorization': `Bearer ${mockData.token}`, - 'Content-Type': 'application/json' - }, - 'method': 'GET' - } // return mocked organisation mockResolvedValueOnce([]) // get maintainer value @@ -68,9 +62,8 @@ it('makes call to expected rpc ', async () => { // return mocked organisation mockResolvedValueOnce([mockData.organisation]) // get maintainer value - const isMaintainer = await isMaintainerOfOrganisation(mockData) - + await isMaintainerOfOrganisation(mockData) // validate call - expect(global.fetch).toBeCalledTimes(1) - expect(global.fetch).toBeCalledWith(expectedUrl, expectedOptions) + expect(global.fetch).toHaveBeenCalledTimes(1) + expect(global.fetch).toHaveBeenCalledWith(expectedUrl, expectedOptions) }) diff --git a/frontend/auth/permissions/isMaintainerOfOrganisation.ts b/frontend/auth/permissions/isMaintainerOfOrganisation.ts index 57b9d71..bf228d4 100644 --- a/frontend/auth/permissions/isMaintainerOfOrganisation.ts +++ b/frontend/auth/permissions/isMaintainerOfOrganisation.ts @@ -1,12 +1,14 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 -import {createJsonHeaders, getBaseUrl} from '../../utils/fetchHelpers' -import logger from '../../utils/logger' +import {OrganisationsOfProject} from '~/types/Project' +import {EditOrganisation, OrganisationsForSoftware, OrganisationSource} from '~/types/Organisation' +import {createJsonHeaders, getBaseUrl} from '~/utils/fetchHelpers' +import logger from '~/utils/logger' import {RsdRole} from '../index' type IsOrganisationMaintainerProps = { @@ -67,13 +69,13 @@ export async function isMaintainerOfOrganisation({organisation, account, token}: } export async function getMaintainerOrganisations({token}: - {token: string}) { + {token?: string}) { try { // without token api request is not needed if (!token) return [] // build url const query = 'rpc/organisations_of_current_maintainer' - let url = `${getBaseUrl()}/${query}` + const url = `${getBaseUrl()}/${query}` const resp = await fetch(url, { method: 'GET', headers: createJsonHeaders(token) @@ -92,5 +94,65 @@ export async function getMaintainerOrganisations({token}: } } +type CanEditOrganisationsProps={ + account?: string + token?: string + organisations: OrganisationsOfProject[]| OrganisationsForSoftware[] +} + +export async function canEditOrganisations({organisations,account,token}:CanEditOrganisationsProps){ + try{ + // collect isMaintainerRequests + const promises:Promise[] = [] + // prepare organisation list + const orgList = organisations.map((item, pos) => { + // save isMaintainer request + promises.push(isMaintainerOfOrganisation({ + organisation: item.id, + account, + token + })) + // extract only needed props + const org: EditOrganisation = { + ...item, + // additional props for edit type + position: pos + 1, + logo_b64: null, + logo_mime_type: null, + source: 'RSD' as OrganisationSource, + status: item.status, + // false by default + canEdit: false + } + return org + }) + // run all isMaintainer requests in parallel + const isMaintainer = await Promise.all(promises) + const canEditOrganisations = orgList.map((item, pos) => { + // update canEdit based on isMaintainer requests + if (isMaintainer[pos]) item.canEdit = isMaintainer[pos] + return item + }) + return canEditOrganisations + }catch(e:any){ + logger(`canEditOrganisations: ${e.message}`, 'error') + // on error all items set to false + return organisations.map((item, pos) => { + // extract only needed props + const org: EditOrganisation = { + ...item, + // additional props for edit type + position: pos + 1, + logo_b64: null, + logo_mime_type: null, + source: 'RSD' as OrganisationSource, + status: item.status, + // false by default + canEdit: false + } + return org + }) + } +} export default isMaintainerOfOrganisation diff --git a/frontend/auth/permissions/isMaintainerOfProject.test.ts b/frontend/auth/permissions/isMaintainerOfProject.test.ts index 1cc9280..80f8237 100644 --- a/frontend/auth/permissions/isMaintainerOfProject.test.ts +++ b/frontend/auth/permissions/isMaintainerOfProject.test.ts @@ -1,5 +1,7 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -34,10 +36,10 @@ it('calls expected rpc', async () => { // return mockResolvedValueOnce(expectedResp) // get maintainer value - const isMaintainer = await isMaintainerOfProject(mockData) + await isMaintainerOfProject(mockData) // validate call - expect(global.fetch).toBeCalledTimes(1) - expect(global.fetch).toBeCalledWith(expectedUrl, expectedOptions) + expect(global.fetch).toHaveBeenCalledTimes(1) + expect(global.fetch).toHaveBeenCalledWith(expectedUrl, expectedOptions) }) it('return isMaintainer true', async () => { diff --git a/frontend/auth/permissions/isMaintainerOfSoftware.test.ts b/frontend/auth/permissions/isMaintainerOfSoftware.test.ts index 32ba3cf..d3428c4 100644 --- a/frontend/auth/permissions/isMaintainerOfSoftware.test.ts +++ b/frontend/auth/permissions/isMaintainerOfSoftware.test.ts @@ -1,5 +1,7 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -34,10 +36,10 @@ it('calls expected rpc', async () => { // return mockResolvedValueOnce(expectedResp) // get maintainer value - const isMaintainer = await isMaintainerOfSoftware(mockData) + await isMaintainerOfSoftware(mockData) // validate call - expect(global.fetch).toBeCalledTimes(1) - expect(global.fetch).toBeCalledWith(expectedUrl, expectedOptions) + expect(global.fetch).toHaveBeenCalledTimes(1) + expect(global.fetch).toHaveBeenCalledWith(expectedUrl, expectedOptions) }) it('return isMaintainer true', async () => { diff --git a/frontend/pages/api/fe/auth/azure.ts b/frontend/auth/providers/azure.ts similarity index 70% rename from frontend/pages/api/fe/auth/azure.ts rename to frontend/auth/providers/azure.ts index 11773d9..05b62b4 100644 --- a/frontend/pages/api/fe/auth/azure.ts +++ b/frontend/auth/providers/azure.ts @@ -4,25 +4,21 @@ // SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) // SPDX-FileCopyrightText: 2022 dv4all -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) // // SPDX-License-Identifier: Apache-2.0 /** - * Azure Active Directory OpenID endpoint + * Azure Active Directory OpenID info * It provides frontend with redirect uri for the login button */ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type {NextApiRequest, NextApiResponse} from 'next' import logger from '~/utils/logger' import {RedirectToProps, getRedirectUrl} from '~/auth/api/authHelpers' import {getAuthEndpoint} from '~/auth/api/authEndpoint' -import {Provider, ApiError} from '.' -type Data = Provider | ApiError - -export async function azureRedirectProps() { +async function azureRedirectProps() { // extract wellknown url from env const wellknownUrl = process.env.AZURE_WELL_KNOWN_URL ?? null if (wellknownUrl) { @@ -59,35 +55,10 @@ export async function azureInfo() { const redirectUrl = getRedirectUrl(redirectProps) // provide redirectUrl and name/label return { - name: process.env.AZURE_DISPLAY_NAME || 'Azure Active Directory', + name: process.env.AZURE_DISPLAY_NAME ?? 'Azure Active Directory', redirectUrl, - html: process.env.AZURE_DESCRIPTION_HTML || 'Login with your institutional credentials.' + html: process.env.AZURE_DESCRIPTION_HTML ?? 'Login with your institutional credentials.' } } return null } - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - try { - // extract all props from env and wellknown endpoint - // and create return url and the name to use in login button - const loginInfo = await azureInfo() - if (loginInfo) { - res.status(200).json(loginInfo) - } else { - res.status(400).json({ - status: 400, - message: 'loginInfo missing' - }) - } - } catch (e: any) { - logger(`api/fe/auth/azure: ${e?.message}`, 'error') - res.status(500).json({ - status: 500, - message: e?.message - }) - } -} diff --git a/frontend/pages/api/fe/auth/helmholtzid.ts b/frontend/auth/providers/helmholtzid.ts similarity index 74% rename from frontend/pages/api/fe/auth/helmholtzid.ts rename to frontend/auth/providers/helmholtzid.ts index b9caff3..c5a3ac7 100644 --- a/frontend/pages/api/fe/auth/helmholtzid.ts +++ b/frontend/auth/providers/helmholtzid.ts @@ -3,25 +3,20 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) // SPDX-FileCopyrightText: 2022 dv4all -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) // // SPDX-License-Identifier: Apache-2.0 /** - * Helmholz ID OpenID endpoint + * Helmholz ID OpenID info * It provides frontend with redirect uri for the login button */ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type {NextApiRequest, NextApiResponse} from 'next' import logger from '~/utils/logger' import {RedirectToProps, getRedirectUrl} from '~/auth/api/authHelpers' import {getAuthEndpoint} from '~/auth/api/authEndpoint' -import {Provider, ApiError} from '.' - -type Data = Provider | ApiError const claims = { id_token: { @@ -36,7 +31,7 @@ async function helmholtzRedirectProps() { const wellknownUrl = process.env.HELMHOLTZID_WELL_KNOWN_URL ?? null if (wellknownUrl) { // get (cached) authorisation endpoint from wellknown url - const authorization_endpoint = await getAuthEndpoint(wellknownUrl, 'helmholtzid') + const authorization_endpoint = await getAuthEndpoint(wellknownUrl, 'helmholtz') if (authorization_endpoint) { // construct all props needed for redirectUrl // use default values if env not provided @@ -78,28 +73,3 @@ export async function helmholtzInfo() { } return null } - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - try { - // extract all props from env and wellknown endpoint - // and create return url and the name to use in login button - const loginInfo = await helmholtzInfo() - if (loginInfo) { - res.status(200).json(loginInfo) - } else { - res.status(400).json({ - status: 400, - message: 'loginInfo missing' - }) - } - } catch (e: any) { - logger(`api/fe/auth/helmholtzid: ${e?.message}`, 'error') - res.status(500).json({ - status: 500, - message: e?.message - }) - } -} diff --git a/frontend/pages/api/fe/auth/linkedin.ts b/frontend/auth/providers/linkedin.ts similarity index 87% rename from frontend/pages/api/fe/auth/linkedin.ts rename to frontend/auth/providers/linkedin.ts index 22ee5ff..c901fb6 100644 --- a/frontend/pages/api/fe/auth/linkedin.ts +++ b/frontend/auth/providers/linkedin.ts @@ -1,26 +1,12 @@ +// SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 +import logger from '~/utils/logger' import {getAuthEndpoint} from '~/auth/api/authEndpoint' import {getRedirectUrl, RedirectToProps} from '~/auth/api/authHelpers' -import logger from '~/utils/logger' - -export async function linkedinInfo() { - const redirectProps = await linkedinRedirectProps() - if (!redirectProps) { - return null - } - - const redirectUrl = getRedirectUrl(redirectProps) - - return { - name: 'LinkedIn', - redirectUrl, - html: 'Sign in with your LinkedIn account' - } -} async function linkedinRedirectProps() { try { @@ -57,8 +43,8 @@ async function linkedinRedirectProps() { authorization_endpoint, redirect_uri, client_id, - scope: 'openid%20profile%20email', - response_mode: 'code' + scope: process.env.LINKEDIN_SCOPES ?? 'openid%20profile%20email', + response_mode: process.env.LINKEDIN_RESPONSE_MODE ?? 'code' } return props } catch (e: any) { @@ -66,3 +52,18 @@ async function linkedinRedirectProps() { return null } } + +export async function linkedinInfo() { + const redirectProps = await linkedinRedirectProps() + if (!redirectProps) { + return null + } + + const redirectUrl = getRedirectUrl(redirectProps) + + return { + name: 'LinkedIn', + redirectUrl, + html: 'Sign in with your LinkedIn account.' + } +} diff --git a/frontend/auth/providers/local.ts b/frontend/auth/providers/local.ts new file mode 100644 index 0000000..e19b168 --- /dev/null +++ b/frontend/auth/providers/local.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center +// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// +// SPDX-License-Identifier: Apache-2.0 + +export function localInfo() { + return { + name: 'Local account', + redirectUrl: '/login/local', + html: '

Sign in with local account is for testing purposes only. This option should not be enabled in the production version.

' + + } +} + diff --git a/frontend/pages/api/fe/auth/orcid.ts b/frontend/auth/providers/orcid.ts similarity index 69% rename from frontend/pages/api/fe/auth/orcid.ts rename to frontend/auth/providers/orcid.ts index 711fdde..509ef52 100644 --- a/frontend/pages/api/fe/auth/orcid.ts +++ b/frontend/auth/providers/orcid.ts @@ -4,7 +4,7 @@ // SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) // SPDX-FileCopyrightText: 2022 dv4all -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) // // SPDX-License-Identifier: Apache-2.0 @@ -13,16 +13,11 @@ * It provides frontend with redirect uri for the login button */ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type {NextApiRequest, NextApiResponse} from 'next' import logger from '~/utils/logger' import {RedirectToProps, getRedirectUrl} from '~/auth/api/authHelpers' import {getAuthEndpoint} from '~/auth/api/authEndpoint' -import {Provider, ApiError} from '.' -type Data = Provider | ApiError - -export async function orcidRedirectProps() { +async function orcidRedirectProps() { try { // extract well known url from env const wellknownUrl = process.env.ORCID_WELL_KNOWN_URL ?? null @@ -66,36 +61,8 @@ export async function orcidInfo() { return { name: 'ORCID', redirectUrl, - html: ` - Sign in with ORCID is supported only for persons approved by the administrators. - Contact us if you wish to login with your ORCID. - ` + html: 'Sign in with your ORCID account.' } } return null } - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - try { - // extract all props from env and wellknown endpoint - // and create return url and the name to use in login button - const loginInfo = await orcidInfo() - if (loginInfo) { - res.status(200).json(loginInfo) - } else { - res.status(400).json({ - status: 400, - message: 'loginInfo missing' - }) - } - } catch (e: any) { - logger(`api/fe/auth/orcid: ${e?.message}`, 'error') - res.status(500).json({ - status: 500, - message: e?.message - }) - } -} diff --git a/frontend/pages/api/fe/auth/surfconext.ts b/frontend/auth/providers/surfconext.ts similarity index 72% rename from frontend/pages/api/fe/auth/surfconext.ts rename to frontend/auth/providers/surfconext.ts index 1ac58c6..60d8a45 100644 --- a/frontend/pages/api/fe/auth/surfconext.ts +++ b/frontend/auth/providers/surfconext.ts @@ -4,23 +4,17 @@ // SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) // SPDX-FileCopyrightText: 2022 dv4all -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) // // SPDX-License-Identifier: Apache-2.0 /** - * SURFconext OpenID endpoint + * SURFconext OpenID info * It provides frontend with redirect uri for the login button */ - -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type {NextApiRequest, NextApiResponse} from 'next' import logger from '~/utils/logger' import {RedirectToProps, getRedirectUrl} from '~/auth/api/authHelpers' import {getAuthEndpoint} from '~/auth/api/authEndpoint' -import {Provider, ApiError} from '.' - -type Data = Provider | ApiError const claims = { id_token: { @@ -30,7 +24,7 @@ const claims = { } } -export async function surfconextRedirectProps() { +async function surfconextRedirectProps() { // extract wellknown url from env const wellknownUrl = process.env.SURFCONEXT_WELL_KNOWN_URL ?? null if (wellknownUrl) { @@ -75,28 +69,3 @@ export async function surfconextInfo() { } return null } - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - try { - // extract all props from env and wellknown endpoint - // and create return url and the name to use in login button - const loginInfo = await surfconextInfo() - if (loginInfo) { - res.status(200).json(loginInfo) - } else { - res.status(400).json({ - status: 400, - message: 'loginInfo missing' - }) - } - } catch (e: any) { - logger(`api/fe/auth/surfconext: ${e?.message}`, 'error') - res.status(500).json({ - status: 500, - message: e?.message - }) - } -} diff --git a/frontend/auth/refreshSession.test.ts b/frontend/auth/refreshSession.test.ts index 2f77959..5908f53 100644 --- a/frontend/auth/refreshSession.test.ts +++ b/frontend/auth/refreshSession.test.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -37,5 +37,5 @@ it('returns null on error and logs error', async () => { // returns null for session expect(session).toEqual(null) // calls error log - expect(global.console.error).toBeCalledTimes(1) + expect(global.console.error).toHaveBeenCalledTimes(1) }) diff --git a/frontend/components/AppFooter/ContactEmail.tsx b/frontend/components/AppFooter/ContactEmail.tsx index fd1f2a1..77846c9 100644 --- a/frontend/components/AppFooter/ContactEmail.tsx +++ b/frontend/components/AppFooter/ContactEmail.tsx @@ -2,6 +2,8 @@ // SPDX-FileCopyrightText: 2022 - 2023 dv4all // SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) // SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -11,7 +13,7 @@ export default function ContactEmail({email, headers}:{email?:string, headers?:s if (email) { let mailTo = email if (headers && headers.length > 0) { - let encodedHeaders: string[] = [] + const encodedHeaders: string[] = [] headers.forEach(d => encodedHeaders.push(encodeURIComponent(d))) mailTo += '?' + headers.join('&') } diff --git a/frontend/components/AppFooter/index.tsx b/frontend/components/AppFooter/index.tsx index c35dee4..ffc8d06 100644 --- a/frontend/components/AppFooter/index.tsx +++ b/frontend/components/AppFooter/index.tsx @@ -19,7 +19,7 @@ export default function AppFooter () { return (