Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collection permissions check #463

Merged
merged 18 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion public/locales/en/createCollection.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,6 @@
"formButtons": {
"save": "Create Collection",
"cancel": "Cancel"
}
},
"notAllowedToCreateCollection": "You do not have permissions to create a collection within this collection."
}
3 changes: 2 additions & 1 deletion public/locales/en/createDataset.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"description": "The collection which contains this data.",
"helpText": "Changing the host collection will clear any fields you may have entered data into.",
"buttonLabel": "Edit Host Collection"
}
},
"notAllowedToCreateDataset": "You do not have permissions to create a dataset within this collection."
}
2 changes: 2 additions & 0 deletions src/collection/domain/models/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ export interface CollectionInputLevel {
include: boolean
required: boolean
}

export const ROOT_COLLECTION_ALIAS = 'root'
9 changes: 9 additions & 0 deletions src/collection/domain/models/CollectionUserPermissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface CollectionUserPermissions {
canAddCollection: boolean
canAddDataset: boolean
canViewUnpublishedCollection: boolean
canEditCollection: boolean
canManageCollectionPermissions: boolean
canPublishCollection: boolean
canDeleteCollection: boolean
}
2 changes: 2 additions & 0 deletions src/collection/domain/repositories/CollectionRepository.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Collection } from '../models/Collection'
import { CollectionUserPermissions } from '../models/CollectionUserPermissions'
import { CollectionDTO } from '../useCases/DTOs/CollectionDTO'

export interface CollectionRepository {
getById: (id: string) => Promise<Collection>
create(collection: CollectionDTO, hostCollection?: string): Promise<number>
getUserPermissions(collectionIdOrAlias: number | string): Promise<CollectionUserPermissions>
}
11 changes: 11 additions & 0 deletions src/collection/domain/useCases/getCollectionUserPermissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CollectionRepository } from '../repositories/CollectionRepository'
import { CollectionUserPermissions } from '../models/CollectionUserPermissions'

export function getCollectionUserPermissions(
collectionRepository: CollectionRepository,
collectionIdOrAlias: number | string
): Promise<CollectionUserPermissions> {
return collectionRepository.getUserPermissions(collectionIdOrAlias).catch((error: Error) => {
throw new Error(error.message)
})
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { CollectionRepository } from '../../domain/repositories/CollectionRepository'
import { Collection } from '../../domain/models/Collection'
import { createCollection, getCollection } from '@iqss/dataverse-client-javascript'
import {
createCollection,
getCollection,
getCollectionUserPermissions
} from '@iqss/dataverse-client-javascript'
import { JSCollectionMapper } from '../mappers/JSCollectionMapper'
import { CollectionDTO } from '../../domain/useCases/DTOs/CollectionDTO'
import { CollectionUserPermissions } from '../../domain/models/CollectionUserPermissions'

export class CollectionJSDataverseRepository implements CollectionRepository {
getById(id: string): Promise<Collection> {
Expand All @@ -16,4 +21,10 @@ export class CollectionJSDataverseRepository implements CollectionRepository {
.execute(collection, hostCollection)
.then((newCollectionIdentifier) => newCollectionIdentifier)
}

getUserPermissions(collectionIdOrAlias: number | string): Promise<CollectionUserPermissions> {
return getCollectionUserPermissions
.execute(collectionIdOrAlias)
.then((jsCollectionUserPermissions) => jsCollectionUserPermissions)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import { DatasetDTO } from '../../domain/useCases/DTOs/DatasetDTO'
import { DatasetDTOMapper } from '../mappers/DatasetDTOMapper'
import { DatasetsWithCount } from '../../domain/models/DatasetsWithCount'
import { VersionUpdateType } from '../../domain/models/VersionUpdateType'
const defaultCollectionId = 'root'
import { ROOT_COLLECTION_ALIAS } from '../../../collection/domain/models/Collection'

const includeDeaccessioned = true
type DatasetDetails = [JSDataset, string[], string, JSDatasetPermissions, JSDatasetLock[]]

Expand Down Expand Up @@ -191,7 +192,7 @@ export class DatasetJSDataverseRepository implements DatasetRepository {

create(
dataset: DatasetDTO,
collectionId = defaultCollectionId
collectionId = ROOT_COLLECTION_ALIAS
): Promise<{ persistentId: string }> {
return createDataset
.execute(DatasetDTOMapper.toJSDatasetDTO(dataset), collectionId)
Expand Down
4 changes: 3 additions & 1 deletion src/sections/Route.enum.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ROOT_COLLECTION_ALIAS } from '../collection/domain/models/Collection'

export enum Route {
HOME = '/',
SIGN_UP = '/dataverseuser.xhtml?editMode=CREATE&redirectPage=%2Fdataverse.xhtml',
Expand All @@ -15,7 +17,7 @@ export enum Route {
export const RouteWithParams = {
COLLECTIONS: (collectionId?: string) => `/collections/${collectionId ?? 'root'}`,
CREATE_COLLECTION: (ownerCollectionId?: string) =>
`/collections/${ownerCollectionId ?? 'root'}/create`
`/collections/${ownerCollectionId ?? ROOT_COLLECTION_ALIAS}/create`
}

export enum QueryParamKey {
Expand Down
19 changes: 17 additions & 2 deletions src/sections/collection/Collection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CollectionSkeleton } from './CollectionSkeleton'
import { CollectionInfo } from './CollectionInfo'
import { Trans, useTranslation } from 'react-i18next'
import { useScrollTop } from '../../shared/hooks/useScrollTop'
import { useGetCollectionUserPermissions } from '../../shared/hooks/useGetCollectionUserPermissions'

interface CollectionProps {
repository: CollectionRepository
Expand All @@ -35,6 +36,16 @@ export function Collection({
useScrollTop()
const { user } = useSession()
const { collection, isLoading } = useCollection(repository, id)
const { collectionUserPermissions } = useGetCollectionUserPermissions({
collectionIdOrAlias: id,
collectionRepository: repository
})

const canUserAddCollection = Boolean(collectionUserPermissions?.canAddCollection)
const canUserAddDataset = Boolean(collectionUserPermissions?.canAddDataset)

const showAddDataActions = user && (canUserAddCollection || canUserAddDataset)

const { t } = useTranslation('collection')

if (!isLoading && !collection) {
Expand Down Expand Up @@ -67,9 +78,13 @@ export function Collection({
/>
</Alert>
)}
{user && (
{showAddDataActions && (
<div className={styles.container}>
<AddDataActionsButton collectionId={id} />
<AddDataActionsButton
collectionId={id}
canAddCollection={canUserAddCollection}
canAddDataset={canUserAddDataset}
/>
</div>
)}
</>
Expand Down
29 changes: 28 additions & 1 deletion src/sections/create-collection/CreateCollection.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert } from '@iqss/dataverse-design-system'
import { useDeepCompareMemo } from 'use-deep-compare'
import { useCollection } from '../collection/useCollection'
import { useGetCollectionMetadataBlocksInfo } from './useGetCollectionMetadataBlocksInfo'
Expand All @@ -25,6 +26,7 @@ import { SeparationLine } from '../shared/layout/SeparationLine/SeparationLine'
import { RequiredFieldText } from '../shared/form/RequiredFieldText/RequiredFieldText'
import { PageNotFound } from '../page-not-found/PageNotFound'
import { CreateCollectionSkeleton } from './CreateCollectionSkeleton'
import { useGetCollectionUserPermissions } from '../../shared/hooks/useGetCollectionUserPermissions'

interface CreateCollectionProps {
ownerCollectionId: string
Expand All @@ -46,6 +48,14 @@ export function CreateCollection({
ownerCollectionId
)

const { collectionUserPermissions, isLoading: isLoadingCollectionUserPermissions } =
useGetCollectionUserPermissions({
collectionIdOrAlias: ownerCollectionId,
collectionRepository: collectionRepository
})

const canUserAddCollection = Boolean(collectionUserPermissions?.canAddCollection)

// TODO:ME In edit mode, collection id should not be from the collection owner but from the collection being edited, but this can perhaps be differentiated by page.
const { metadataBlocksInfo, isLoading: isLoadingMetadataBlocksInfo } =
useGetCollectionMetadataBlocksInfo({
Expand Down Expand Up @@ -94,14 +104,20 @@ export function CreateCollection({
)

useEffect(() => {
if (!isLoadingCollection && !isLoadingMetadataBlocksInfo && !isLoadingAllMetadataBlocksInfo) {
if (
!isLoadingCollection &&
!isLoadingMetadataBlocksInfo &&
!isLoadingAllMetadataBlocksInfo &&
!isLoadingCollectionUserPermissions
) {
setIsLoading(false)
}
}, [
isLoading,
isLoadingCollection,
isLoadingMetadataBlocksInfo,
isLoadingAllMetadataBlocksInfo,
isLoadingCollectionUserPermissions,
setIsLoading
])

Expand All @@ -113,11 +129,22 @@ export function CreateCollection({
isLoadingCollection ||
isLoadingMetadataBlocksInfo ||
isLoadingAllMetadataBlocksInfo ||
isLoadingCollectionUserPermissions ||
!collection
) {
return <CreateCollectionSkeleton />
}

if (collectionUserPermissions && !canUserAddCollection) {
return (
<div className="pt-4" data-testid="not-allowed-to-create-collection-alert">
<Alert variant="danger" dismissible={false}>
{t('notAllowedToCreateCollection')}
</Alert>
</div>
)
}

const formDefaultValues: CollectionFormData = {
hostCollection: collection.name,
name: user?.displayName ? `${user?.displayName} Collection` : '',
Expand Down
3 changes: 2 additions & 1 deletion src/sections/create-collection/CreateCollectionFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ReactElement } from 'react'
import { useParams } from 'react-router-dom'
import { CollectionJSDataverseRepository } from '../../collection/infrastructure/repositories/CollectionJSDataverseRepository'
import { CreateCollection } from './CreateCollection'
import { ROOT_COLLECTION_ALIAS } from '../../collection/domain/models/Collection'
import { MetadataBlockInfoJSDataverseRepository } from '../../metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository'

const collectionRepository = new CollectionJSDataverseRepository()
Expand All @@ -14,7 +15,7 @@ export class CreateCollectionFactory {
}

function CreateCollectionWithParams() {
const { ownerCollectionId = 'root' } = useParams<{ ownerCollectionId: string }>()
const { ownerCollectionId = ROOT_COLLECTION_ALIAS } = useParams<{ ownerCollectionId: string }>()

return (
<CreateCollection
Expand Down
33 changes: 32 additions & 1 deletion src/sections/create-dataset/CreateDataset.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,56 @@
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert } from '@iqss/dataverse-design-system'
import { type DatasetRepository } from '../../dataset/domain/repositories/DatasetRepository'
import { type MetadataBlockInfoRepository } from '../../metadata-block-info/domain/repositories/MetadataBlockInfoRepository'
import { SeparationLine } from '../shared/layout/SeparationLine/SeparationLine'
import { HostCollectionForm } from './HostCollectionForm/HostCollectionForm'
import { NotImplementedModal } from '../not-implemented/NotImplementedModal'
import { useNotImplementedModal } from '../not-implemented/NotImplementedModalContext'
import { DatasetMetadataForm } from '../shared/form/DatasetMetadataForm'
import { useGetCollectionUserPermissions } from '../../shared/hooks/useGetCollectionUserPermissions'
import { CollectionRepository } from '../../collection/domain/repositories/CollectionRepository'
import { useLoading } from '../loading/LoadingContext'
import { ROOT_COLLECTION_ALIAS } from '../../collection/domain/models/Collection'

interface CreateDatasetProps {
datasetRepository: DatasetRepository
metadataBlockInfoRepository: MetadataBlockInfoRepository
collectionRepository: CollectionRepository
collectionId?: string
}

export function CreateDataset({
datasetRepository,
metadataBlockInfoRepository,
collectionId = 'root'
collectionRepository,
collectionId = ROOT_COLLECTION_ALIAS
}: CreateDatasetProps) {
const { t } = useTranslation('createDataset')
const { isModalOpen, hideModal } = useNotImplementedModal()
const { setIsLoading } = useLoading()

const { collectionUserPermissions, isLoading: isLoadingCollectionUserPermissions } =
useGetCollectionUserPermissions({
collectionIdOrAlias: collectionId,
collectionRepository: collectionRepository
})

const canUserAddDataset = Boolean(collectionUserPermissions?.canAddDataset)

useEffect(() => {
setIsLoading(isLoadingCollectionUserPermissions)
}, [isLoadingCollectionUserPermissions, setIsLoading])

if (collectionUserPermissions && !canUserAddDataset) {
return (
<div className="pt-4" data-testid="not-allowed-to-create-dataset-alert">
<Alert variant="danger" dismissible={false}>
{t('notAllowedToCreateDataset')}
</Alert>
</div>
)
}

return (
<>
Expand Down
3 changes: 3 additions & 0 deletions src/sections/create-dataset/CreateDatasetFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { CreateDataset } from './CreateDataset'
import { DatasetJSDataverseRepository } from '../../dataset/infrastructure/repositories/DatasetJSDataverseRepository'
import { MetadataBlockInfoJSDataverseRepository } from '../../metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository'
import { NotImplementedModalProvider } from '../not-implemented/NotImplementedModalProvider'
import { CollectionJSDataverseRepository } from '../../collection/infrastructure/repositories/CollectionJSDataverseRepository'

const datasetRepository = new DatasetJSDataverseRepository()
const metadataBlockInfoRepository = new MetadataBlockInfoJSDataverseRepository()
const collectionRepository = new CollectionJSDataverseRepository()

export class CreateDatasetFactory {
static create(): ReactElement {
Expand All @@ -26,6 +28,7 @@ function CreateDatasetWithSearchParams() {
<CreateDataset
datasetRepository={datasetRepository}
metadataBlockInfoRepository={metadataBlockInfoRepository}
collectionRepository={collectionRepository}
collectionId={collectionId}
/>
)
Expand Down
37 changes: 7 additions & 30 deletions src/sections/layout/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
import dataverse_logo from '../../../assets/dataverse_brand_icon.svg'
import { useTranslation } from 'react-i18next'
import { Navbar } from '@iqss/dataverse-design-system'
import { Route, RouteWithParams } from '../../Route.enum'
import { Route } from '../../Route.enum'
import { useSession } from '../../session/SessionContext'
import { Link, useNavigate } from 'react-router-dom'
import { BASE_URL } from '../../../config'
import { LoggedInHeaderActions } from './LoggedInHeaderActions'
import { CollectionJSDataverseRepository } from '../../../collection/infrastructure/repositories/CollectionJSDataverseRepository'

const collectionRepository = new CollectionJSDataverseRepository()

const currentPage = 0
export function Header() {
const { t } = useTranslation('header')
const { user, logout } = useSession()
const navigate = useNavigate()

const onLogoutClick = () => {
void logout().then(() => {
navigate(currentPage)
})
}

const createCollectionRoute = RouteWithParams.CREATE_COLLECTION()
const { user } = useSession()

return (
<Navbar
Expand All @@ -28,21 +21,7 @@ export function Header() {
logoImgSrc: dataverse_logo
}}>
{user ? (
<>
<Navbar.Dropdown title={t('navigation.addData')} id="dropdown-addData">
<Navbar.Dropdown.Item as={Link} to={createCollectionRoute}>
{t('navigation.newCollection')}
</Navbar.Dropdown.Item>
<Navbar.Dropdown.Item as={Link} to={Route.CREATE_DATASET}>
{t('navigation.newDataset')}
</Navbar.Dropdown.Item>
</Navbar.Dropdown>
<Navbar.Dropdown title={user.displayName} id="dropdown-user">
<Navbar.Dropdown.Item href="#" onClick={onLogoutClick}>
{t('logOut')}
</Navbar.Dropdown.Item>
</Navbar.Dropdown>
</>
<LoggedInHeaderActions user={user} collectionRepository={collectionRepository} />
) : (
<>
<Navbar.Link href={`${BASE_URL}${Route.LOG_IN}`}>{t('logIn')}</Navbar.Link>
Expand All @@ -52,5 +31,3 @@ export function Header() {
</Navbar>
)
}

// TODO: AddData Dropdown item needs proper permissions checking, see Spike #318
Loading
Loading