diff --git a/website/src/components/Group/GroupForm.tsx b/website/src/components/Group/GroupForm.tsx index f724b0a34c..81bd5d43de 100644 --- a/website/src/components/Group/GroupForm.tsx +++ b/website/src/components/Group/GroupForm.tsx @@ -88,7 +88,7 @@ export const GroupForm: FC = ({ title, buttonText, defaultGroupD
- + diff --git a/website/src/components/User/GroupPage.spec.tsx b/website/src/components/User/GroupPage.spec.tsx new file mode 100644 index 0000000000..e1aa77d424 --- /dev/null +++ b/website/src/components/User/GroupPage.spec.tsx @@ -0,0 +1,75 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, test } from 'vitest'; + +import { GroupPage } from './GroupPage'; +import { testAccessToken, testConfig, testDatabaseName, testGroups, testUser } from '../../../vitest.setup'; + +const DUMMY_GROUP_DETAILS = { + group: testGroups[0], + users: [testUser], +}; + +describe('GroupPage', () => { + test('test authenticated can edit view', () => { + render( + , + ); + + const targetString = `to ${testDatabaseName} see full group details`; + expect(screen.queryByText(targetString)).toBeNull(); + expect(screen.getByText(testGroups[0].contactEmail!)).toBeVisible(); + expect(screen.getByRole('heading', { name: `Sequences available in ${testDatabaseName}` })).toBeVisible(); + expect(screen.getByRole('heading', { name: 'Users' })).toBeVisible(); + }); + + test('test authenticated cannot edit view', () => { + render( + , + ); + + const targetString = `to ${testDatabaseName} see full group details`; + expect(screen.queryByText(targetString)).toBeNull(); + expect(screen.getByText(testGroups[0].contactEmail!)).toBeVisible(); + expect(screen.getByRole('heading', { name: `Sequences available in ${testDatabaseName}` })).toBeVisible(); + expect(screen.queryByRole('heading', { name: /users/i })).toBeNull(); + }); + + test('test unauthenticated view', () => { + render( + , + ); + + const targetString = `to ${testDatabaseName} to see contact details for the group.`; + expect(screen.queryByText(targetString)).toBeVisible(); + expect(screen.queryByText(testGroups[0].contactEmail!)).toBeNull(); + expect(screen.getByRole('heading', { name: `Sequences available in ${testDatabaseName}` })).toBeVisible(); + expect(screen.queryByRole('heading', { name: /users/i })).toBeNull(); + }); +}); diff --git a/website/src/components/User/GroupPage.tsx b/website/src/components/User/GroupPage.tsx index 628e54f6bb..545e70c92d 100644 --- a/website/src/components/User/GroupPage.tsx +++ b/website/src/components/User/GroupPage.tsx @@ -21,12 +21,13 @@ import IwwaArrowDown from '~icons/iwwa/arrow-down'; type GroupPageProps = { prefetchedGroupDetails: GroupDetails; clientConfig: ClientConfig; - accessToken: string; + accessToken: string | undefined; username: string; userGroups: Group[]; organisms: Organism[]; databaseName: string; continueSubmissionIntent?: ContinueSubmissionIntent; + loginUrl: string; }; const InnerGroupPage: FC = ({ @@ -38,6 +39,7 @@ const InnerGroupPage: FC = ({ organisms, databaseName, continueSubmissionIntent, + loginUrl, }) => { const groupName = prefetchedGroupDetails.group.groupName; const groupId = prefetchedGroupDetails.group.groupId; @@ -58,7 +60,7 @@ const InnerGroupPage: FC = ({ setNewUserName(''); }; - const userIsGroupMember = groupDetails.data?.users.some((user) => user.name === username) ?? false; + const userIsGroupMember = groupDetails.data?.users?.some((user) => user.name === username) ?? false; const userHasEditPrivileges = userGroups.some((group) => group.groupId === prefetchedGroupDetails.group.groupId); const { data: sequenceCounts, isLoading: sequenceCountsLoading } = useQuery({ @@ -152,7 +154,7 @@ const InnerGroupPage: FC = ({
) : ( -

- - {groupName} +

+

)} @@ -183,12 +184,24 @@ const InnerGroupPage: FC = ({ {groupDetails.data?.group.groupId} {groupDetails.data?.group.institution} - {groupDetails.data?.group.contactEmail} + {accessToken && ( + {groupDetails.data?.group.contactEmail} + )} +
+ {!accessToken && ( + + + Log in + {' '} + to {databaseName} to see contact details for the group. + + )} +
@@ -233,7 +246,7 @@ const InnerGroupPage: FC = ({
    - {groupDetails.data?.users.map((user) => ( + {groupDetails.data?.users?.map((user) => (
  • {user.name} {user.name !== username && ( diff --git a/website/src/hooks/useGroupOperations.ts b/website/src/hooks/useGroupOperations.ts index d19c1488b7..3a2d7f9fd7 100644 --- a/website/src/hooks/useGroupOperations.ts +++ b/website/src/hooks/useGroupOperations.ts @@ -10,7 +10,7 @@ import { stringifyMaybeAxiosError } from '../utils/stringifyMaybeAxiosError.ts'; type UseGroupOperationsProps = { clientConfig: ClientConfig; - accessToken: string; + accessToken: string | undefined; setErrorMessage: (message?: string) => void; }; @@ -162,7 +162,7 @@ function callEditGroup(accessToken: string, zodios: ZodiosInstance void, zodios: ZodiosInstance, refetchGroups: () => Promise, @@ -185,7 +185,7 @@ function callRemoveFromGroup( } function callAddToGroup( - accessToken: string, + accessToken: string | undefined, openErrorFeedback: (message: string | undefined) => void, zodios: ZodiosInstance, refetchGroups: () => Promise, diff --git a/website/src/pages/group/[groupId]/edit.astro b/website/src/pages/group/[groupId]/edit.astro index 3658e716b0..1f93a1c95d 100644 --- a/website/src/pages/group/[groupId]/edit.astro +++ b/website/src/pages/group/[groupId]/edit.astro @@ -19,7 +19,7 @@ if (isNaN(groupId)) { } const groupManagementClient = GroupManagementClient.create(); -const groupDetailsResult = await groupManagementClient.getGroupDetails(accessToken, groupId); +const groupDetailsResult = await groupManagementClient.getGroupDetails(groupId, accessToken); --- diff --git a/website/src/pages/group/[groupId]/index.astro b/website/src/pages/group/[groupId]/index.astro index 187c08b34f..37124daca3 100644 --- a/website/src/pages/group/[groupId]/index.astro +++ b/website/src/pages/group/[groupId]/index.astro @@ -1,20 +1,21 @@ --- import { GroupPage } from '../../../components/User/GroupPage'; import ErrorBox from '../../../components/common/ErrorBox.tsx'; -import NeedToLogin from '../../../components/common/NeedToLogin.astro'; import { getConfiguredOrganisms, getRuntimeConfig, getWebsiteConfig } from '../../../config'; import BaseLayout from '../../../layouts/BaseLayout.astro'; import type { ContinueSubmissionIntent } from '../../../routes/routes'; import { GroupManagementClient } from '../../../services/groupManagementClient'; import { getAccessToken } from '../../../utils/getAccessToken'; +import { getAuthUrl } from '../../../utils/getAuthUrl'; -const session = Astro.locals.session!; -const accessToken = getAccessToken(session)!; -const username = session.user?.username ?? ''; +const session = Astro.locals.session; +const accessToken = getAccessToken(session); +const username = session?.user?.username ?? ''; const groupId = parseInt(Astro.params.groupId!, 10); const clientConfig = getRuntimeConfig().public; const organisms = getConfiguredOrganisms(); const databaseName = getWebsiteConfig().name; +const loginUrl = await getAuthUrl(Astro.url.toString()); const continueSubmissionOrganism = Astro.url.searchParams.get('continueSubmissionOrganism'); @@ -28,7 +29,7 @@ if (isNaN(groupId)) { } const groupManagementClient = GroupManagementClient.create(); -const groupDetailsResult = await groupManagementClient.getGroupDetails(accessToken, groupId); +const groupDetailsResult = await groupManagementClient.getGroupDetails(groupId, accessToken); const userGroupsResponse = await groupManagementClient.getGroupsOfUser(accessToken); const userGroups = userGroupsResponse.match( (groups) => groups, @@ -44,25 +45,22 @@ const userGroups = userGroupsResponse.match( activeTopNavigationItem='account' > { - !accessToken ? ( - - ) : ( - groupDetailsResult.match( - (groupDetails) => ( - - ), - () => Failed to fetch group details, sorry for the inconvenience!, - ) + groupDetailsResult.match( + (groupDetails) => ( + + ), + () => Failed to fetch group details, sorry for the inconvenience!, ) } diff --git a/website/src/services/commonApiTypes.ts b/website/src/services/commonApiTypes.ts index f718ed8056..b1cc662dba 100644 --- a/website/src/services/commonApiTypes.ts +++ b/website/src/services/commonApiTypes.ts @@ -11,6 +11,14 @@ export const [authorizationHeader] = makeParameters([ }, ]); +export const [optionalAuthorizationHeader] = makeParameters([ + { + name: 'Authorization', + type: 'Header', + schema: z.string().includes('Bearer ', { position: 0 }).optional(), + }, +]); + export function withOrganismPathSegment(path: Path) { return `/:organism${path}` as const; } diff --git a/website/src/services/groupManagementApi.ts b/website/src/services/groupManagementApi.ts index a94399aff2..2ec275e779 100644 --- a/website/src/services/groupManagementApi.ts +++ b/website/src/services/groupManagementApi.ts @@ -1,7 +1,12 @@ import { makeApi, makeEndpoint } from '@zodios/core'; import z from 'zod'; -import { authorizationHeader, conflictError, notAuthorizedError } from './commonApiTypes.ts'; +import { + authorizationHeader, + conflictError, + notAuthorizedError, + optionalAuthorizationHeader, +} from './commonApiTypes.ts'; import { group, groupDetails, newGroup } from '../types/backend.ts'; const createGroupEndpoint = makeEndpoint({ method: 'post', @@ -53,9 +58,8 @@ const getGroupDetailsEndpoint = makeEndpoint({ method: 'get', path: '/groups/:groupId', alias: 'getGroupDetails', - parameters: [authorizationHeader], + parameters: [optionalAuthorizationHeader], response: groupDetails, - errors: [notAuthorizedError], }); const getGroupsOfUserEndpoint = makeEndpoint({ method: 'get', diff --git a/website/src/services/groupManagementClient.ts b/website/src/services/groupManagementClient.ts index 17c67b2d9c..f7120f9877 100644 --- a/website/src/services/groupManagementClient.ts +++ b/website/src/services/groupManagementClient.ts @@ -19,13 +19,13 @@ export class GroupManagementClient extends ZodiosWrapperClient; @@ -307,11 +307,17 @@ export type Group = z.infer; export const groupDetails = z.object({ group, - users: z.array( - z.object({ - name: z.string(), - }), - ), + /** + * List of users in the group. + * Null when the requesting user is not authenticated or not authorized to view members. + */ + users: z + .array( + z.object({ + name: z.string(), + }), + ) + .nullable(), }); export type GroupDetails = z.infer; diff --git a/website/src/utils/createAuthorizationHeader.ts b/website/src/utils/createAuthorizationHeader.ts index 43bb286343..aff5a1e886 100644 --- a/website/src/utils/createAuthorizationHeader.ts +++ b/website/src/utils/createAuthorizationHeader.ts @@ -1,3 +1,3 @@ -export function createAuthorizationHeader(token: string) { - return { Authorization: `Bearer ${token}` }; // eslint-disable-line @typescript-eslint/naming-convention +export function createAuthorizationHeader(token?: string) { + return token ? { Authorization: `Bearer ${token}` } : undefined; // eslint-disable-line @typescript-eslint/naming-convention } diff --git a/website/vitest.setup.ts b/website/vitest.setup.ts index 47361a333b..a8208124ed 100755 --- a/website/vitest.setup.ts +++ b/website/vitest.setup.ts @@ -23,6 +23,8 @@ export const DEFAULT_GROUP_NAME = 'testGroup'; export const testOrganism = 'testOrganism'; +export const testDatabaseName = 'testDatabase'; + export const testConfig = { public: { discriminator: 'client', @@ -313,6 +315,8 @@ const lapisRequestMocks = { }, }; +export const testUser = { name: 'testUser' }; + export const testGroups: Group[] = [ { groupId: 1,