Skip to content

Commit

Permalink
feat: sanitize future installation terms of use
Browse files Browse the repository at this point in the history
  • Loading branch information
g-saracca committed Dec 3, 2024
1 parent 4818ec0 commit 6f640ca
Show file tree
Hide file tree
Showing 16 changed files with 67 additions and 34 deletions.
2 changes: 1 addition & 1 deletion src/info/domain/models/TermsOfUse.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type TermsOfUse = string | null
export type TermsOfUse = string
2 changes: 1 addition & 1 deletion src/info/domain/repositories/DataverseInfoRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { TermsOfUse } from '../models/TermsOfUse'

export interface DataverseInfoRepository {
getVersion(): Promise<DataverseVersion>
getTermsOfUse: () => Promise<TermsOfUse>
getApiTermsOfUse: () => Promise<TermsOfUse>
}
4 changes: 2 additions & 2 deletions src/info/domain/useCases/getTermsOfUse.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { type TermsOfUse } from '../../../info/domain/models/TermsOfUse'
import { DataverseInfoRepository } from '../repositories/DataverseInfoRepository'

export function getTermsOfUse(
export function getApiTermsOfUse(
dataverseInfoRepository: DataverseInfoRepository
): Promise<TermsOfUse> {
return dataverseInfoRepository.getTermsOfUse().catch((error) => {
return dataverseInfoRepository.getApiTermsOfUse().catch((error) => {
throw error
})
}
18 changes: 18 additions & 0 deletions src/info/infrastructure/mappers/JSTermsOfUseMapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import DOMPurify from 'dompurify'
import { TermsOfUse } from '@/info/domain/models/TermsOfUse'

export class JSTermsOfUseMapper {
static toSanitizedTermsOfUse(jsTermsOfUse: TermsOfUse): TermsOfUse {
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
// set all elements owning target to target=_blank and rel=noopener for security reasons. See https://developer.chrome.com/docs/lighthouse/best-practices/external-anchors-use-rel-noopener
if ('target' in node) {
node.setAttribute('target', '_blank')
node.setAttribute('rel', 'noopener')
}
})
// DOMPurify docs 👉 https://github.com/cure53/DOMPurify
const cleanedHTML = DOMPurify.sanitize(jsTermsOfUse, { USE_PROFILES: { html: true } })

return cleanedHTML
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { axiosInstance } from '@/axiosInstance'
import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository'
import { DataverseVersion } from '@/info/domain/models/DataverseVersion'
import { TermsOfUse } from '@/info/domain/models/TermsOfUse'
import { JSTermsOfUseMapper } from '../mappers/JSTermsOfUseMapper'

interface JSDataverseDataverseVersion {
number: string
Expand All @@ -29,12 +30,12 @@ export class DataverseInfoJSDataverseRepository implements DataverseInfoReposito
})
}

async getTermsOfUse() {
async getApiTermsOfUse() {
//TODO - implement using js-dataverse
const response = await axiosInstance.get<{ data: { message: TermsOfUse } }>(
'/api/v1/info/apiTermsOfUse',
{ excludeToken: true }
)
return response.data.data.message
return JSTermsOfUseMapper.toSanitizedTermsOfUse(response.data.data.message)
}
}
1 change: 0 additions & 1 deletion src/sections/sign-up/SignUp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { ValidTokenNotLinkedAccountForm } from './valid-token-not-linked-account
import styles from './SignUp.module.scss'

// TODO:ME - All use cases will return same error message so this is blocking us for making requests to other public use cases like get root collection, should work removing access token from localstorage but we need it for future call
// TODO:ME - Ask about the format of the terms of use, html string? just text string? what is shown in the box if there is just a url string ?

interface SignUpProps {
userRepository: UserRepository
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import 'node_modules/@iqss/dataverse-design-system/src/lib/assets/styles/design-tokens/colors.module';

.form-container {
scroll-margin-top: 62px;
}
Expand All @@ -15,3 +17,12 @@
text-align: left;
}
}

.terms-of-use-wrapper {
max-height: 200px;
padding: 12px;
overflow-y: auto;
background-color: #f5f5f5;
border: solid 1px $dv-border-color;
border-radius: 6px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,11 @@ export const FormFields = ({ userRepository, formDefaultValues, termsOfUse }: Fo
render={({ field: { onChange, ref, value }, fieldState: { invalid, error } }) => (
<Col md={6}>
<Stack direction="vertical" gap={3}>
<Form.Group.TextArea
value={termsOfUse ? termsOfUse : t('fields.termsAccepted.noTerms')}
rows={2}
disabled
<div
className={styles['terms-of-use-wrapper']}
dangerouslySetInnerHTML={{
__html: termsOfUse ? termsOfUse : t('fields.termsAccepted.noTerms')
}}
/>

<Form.Group.Checkbox
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useContext } from 'react'
import { AuthContext } from 'react-oauth2-code-pkce'
import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository'
import { UserRepository } from '@/users/domain/repositories/UserRepository'
import { useGetTermsOfUse } from '@/shared/hooks/useGetTermsOfUse'
import { useGetApiTermsOfUse } from '@/shared/hooks/useGetApiTermsOfUse'
import { OIDC_STANDARD_CLAIMS, type ValidTokenNotLinkedAccountFormData } from './types'
import { ValidTokenNotLinkedAccountFormHelper } from './ValidTokenNotLinkedAccountFormHelper'
import { FormFields } from './FormFields'
Expand All @@ -18,7 +18,8 @@ export const ValidTokenNotLinkedAccountForm = ({
dataverseInfoRepository
}: ValidTokenNotLinkedAccountFormProps) => {
const { tokenData } = useContext(AuthContext)
const { termsOfUse, isLoading: isLoadingTermsOfUse } = useGetTermsOfUse(dataverseInfoRepository)
const { termsOfUse, isLoading: isLoadingTermsOfUse } =
useGetApiTermsOfUse(dataverseInfoRepository)

const defaultUserName =
ValidTokenNotLinkedAccountFormHelper.getTokenDataValue<string>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface UseGetTermsOfUseReturnType {
isLoading: boolean
}

export const useGetTermsOfUse = (
export const useGetApiTermsOfUse = (
dataverseInfoRepository: DataverseInfoRepository
): UseGetTermsOfUseReturnType => {
const [termsOfUse, setTermsOfUse] = useState<string | null>(null)
Expand All @@ -18,7 +18,7 @@ export const useGetTermsOfUse = (
const handleGetUseOfTerms = async () => {
setIsLoading(true)
try {
const termsOfUse = await dataverseInfoRepository.getTermsOfUse()
const termsOfUse = await dataverseInfoRepository.getApiTermsOfUse()

setTermsOfUse(termsOfUse)
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class DataverseInfoMockLoadingRepository implements DataverseInfoMockRepo
return new Promise(() => {})
}

getTermsOfUse(): Promise<TermsOfUse> {
getApiTermsOfUse(): Promise<TermsOfUse> {
return new Promise(() => {})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class DataverseInfoMockRepository implements DataverseInfoRepository {
})
}

getTermsOfUse(): Promise<TermsOfUse> {
getApiTermsOfUse(): Promise<TermsOfUse> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(TermsOfUseMother.create())
Expand Down
4 changes: 1 addition & 3 deletions tests/component/info/models/TermsOfUseMother.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { faker } from '@faker-js/faker'
import isChromatic from 'chromatic/isChromatic'
import { TermsOfUse } from '@/info/domain/models/TermsOfUse'

export class TermsOfUseMother {
static create(): TermsOfUse {
return isChromatic() ? 'https://some-terms-of-use-url.com' : faker.lorem.paragraphs(8)
return '<h3>Terms of Use SPA dev</h3><p>Please see our full <a href="https://beta.dataverse.org/spa/">terms of use</a></p><p onclick="alert(\'this alert is to text sanitization\')">Thanks for reading!</p>'
}

static createEmpty(): TermsOfUse {
Expand Down
2 changes: 1 addition & 1 deletion tests/component/sections/sign-up/SignUp.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const userRepository: UserRepository = {} as UserRepository

describe('SignUp', () => {
beforeEach(() => {
dataverseInfoRepository.getTermsOfUse = cy.stub().resolves('Terms of use')
dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves('Terms of use')
})

it('renders the valid token not linked account form and correct alerts when hasValidTokenButNotLinkedAccount prop is true', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ import { UserRepository } from '@/users/domain/repositories/UserRepository'
import { ValidTokenNotLinkedAccountForm } from '@/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm'
import { AuthContextMother } from '@tests/component/auth/AuthContextMother'
import { UserDTO } from '@/users/domain/useCases/DTOs/UserDTO'
import { TermsOfUseMother } from '@tests/component/info/models/TermsOfUseMother'
import { JSTermsOfUseMapper } from '@/info/infrastructure/mappers/JSTermsOfUseMapper'

const dataverseInfoRepository: DataverseInfoRepository = {} as DataverseInfoRepository
const userRepository: UserRepository = {} as UserRepository

const termsOfUseMock = 'Terms of use'
const termsOfUseMock = TermsOfUseMother.create()
const sanitizedTermsOfUseMock = JSTermsOfUseMapper.toSanitizedTermsOfUse(termsOfUseMock)

const mockUserName = 'mockUserName'
const mockFirstName = 'mockFirstName'
const mockLastName = 'mockLastName'
const mockEmail = '[email protected]'

describe('ValidTokenNotLinkedAccountForm', () => {
beforeEach(() => {
dataverseInfoRepository.getTermsOfUse = cy.stub().resolves(termsOfUseMock)
dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves(sanitizedTermsOfUseMock)
userRepository.register = cy.stub().as('registerUser').resolves()
})

Expand Down Expand Up @@ -51,7 +55,7 @@ describe('ValidTokenNotLinkedAccountForm', () => {
cy.findByLabelText('Given Name').should('have.value', mockFirstName)
cy.findByLabelText('Family Name').should('have.value', mockLastName)
cy.findByLabelText('Email').should('have.value', mockEmail)
cy.findByText(termsOfUseMock).should('exist')
cy.findByText('Terms of Use SPA dev').should('exist')
})

it('renders the form fields with the correct default values when tokenData does not have preferred username, given name, family name and email', () => {
Expand Down Expand Up @@ -79,7 +83,7 @@ describe('ValidTokenNotLinkedAccountForm', () => {
cy.findByLabelText('Given Name').should('have.value', '')
cy.findByLabelText('Family Name').should('have.value', '')
cy.findByLabelText('Email').should('have.value', '')
cy.findByText(termsOfUseMock).should('exist')
cy.findByText('Terms of Use SPA dev').should('exist')
})
})

Expand Down Expand Up @@ -216,7 +220,7 @@ describe('ValidTokenNotLinkedAccountForm', () => {
})

it('shows no terms message when there are no terms of use', () => {
dataverseInfoRepository.getTermsOfUse = cy.stub().resolves(null)
dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves(null)

cy.customMount(
<AuthContext.Provider
Expand Down
16 changes: 8 additions & 8 deletions tests/component/shared/hooks/useGetTermsOfUse.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { act, renderHook } from '@testing-library/react'
import { useGetTermsOfUse } from '@/shared/hooks/useGetTermsOfUse'
import { useGetApiTermsOfUse } from '@/shared/hooks/useGetApiTermsOfUse'
import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository'
import { TermsOfUseMother } from '@tests/component/info/models/TermsOfUseMother'

const dataverseInfoRepository: DataverseInfoRepository = {} as DataverseInfoRepository
const termsOfUseMock = TermsOfUseMother.create()

describe('useGetTermsOfUse', () => {
describe('useGetApiTermsOfUse', () => {
it('should return terms of use correctly', async () => {
dataverseInfoRepository.getTermsOfUse = cy.stub().resolves(termsOfUseMock)
dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves(termsOfUseMock)

const { result } = renderHook(() => useGetTermsOfUse(dataverseInfoRepository))
const { result } = renderHook(() => useGetApiTermsOfUse(dataverseInfoRepository))

await act(() => {
expect(result.current.isLoading).to.deep.equal(true)
Expand All @@ -26,9 +26,9 @@ describe('useGetTermsOfUse', () => {

describe('Error handling', () => {
it('should return correct error message when there is an error type catched', async () => {
dataverseInfoRepository.getTermsOfUse = cy.stub().rejects(new Error('Error message'))
dataverseInfoRepository.getApiTermsOfUse = cy.stub().rejects(new Error('Error message'))

const { result } = renderHook(() => useGetTermsOfUse(dataverseInfoRepository))
const { result } = renderHook(() => useGetApiTermsOfUse(dataverseInfoRepository))

await act(() => {
expect(result.current.isLoading).to.deep.equal(true)
Expand All @@ -42,9 +42,9 @@ describe('useGetTermsOfUse', () => {
})

it('should return correct error message when there is not an error type catched', async () => {
dataverseInfoRepository.getTermsOfUse = cy.stub().rejects('Error message')
dataverseInfoRepository.getApiTermsOfUse = cy.stub().rejects('Error message')

const { result } = renderHook(() => useGetTermsOfUse(dataverseInfoRepository))
const { result } = renderHook(() => useGetApiTermsOfUse(dataverseInfoRepository))

await act(() => {
expect(result.current.isLoading).to.deep.equal(true)
Expand Down

0 comments on commit 6f640ca

Please sign in to comment.