Skip to content

Commit

Permalink
SSO - Correction des erreurs de connexion (#3763)
Browse files Browse the repository at this point in the history
## Linked issues

- Resolve #3759
- Suppression de ApiError pour n'utiliser que FrontendApiError
- Utilisation d'un `<RequireAuth/>`, sur le même principe que sur
MonitorEnv

----

- [ ] Tests E2E (Cypress)
  • Loading branch information
louptheron authored Oct 17, 2024
2 parents ad89c3a + fbbf802 commit 8ff6e6c
Show file tree
Hide file tree
Showing 64 changed files with 731 additions and 385 deletions.
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ run-front-for-cypress:
run-cypress:
cd ./frontend && npm run test:e2e:open

.PHONY: run-puppeteer ##TEST ▶️ Run Puppeteer 📝
run-puppeteer:
cd ./frontend && npm run test:multi-windows:open

test-back: check-clean-archi
@if [ -z "$(class)" ]; then \
echo "Running all Backend tests..."; \
Expand All @@ -152,7 +156,7 @@ test-back-watch:
run-back-for-puppeteer: docker-env run-stubbed-apis
docker compose up -d --quiet-pull --wait db
docker compose -f ./infra/docker/docker-compose.puppeteer.yml up -d monitorenv-app
cd backend && MONITORENV_URL=http://localhost:9880 ./gradlew bootRun --args='--spring.profiles.active=local --spring.config.additional-location=$(INFRA_FOLDER)'
cd backend && MONITORFISH_OIDC_ENABLED=false MONITORENV_URL=http://localhost:9880 ./gradlew bootRun --args='--spring.profiles.active=local --spring.config.additional-location=$(INFRA_FOLDER)'

.PHONY: run-front-for-puppeteer ##TEST ▶️ Run frontend when using Puppeteer 📝
run-front-for-puppeteer:
Expand Down
2 changes: 1 addition & 1 deletion frontend/.env.local.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ FRONTEND_OIDC_AUTHORITY=http://localhost:8085/realms/monitor
FRONTEND_OIDC_CLIENT_ID=monitorfish
FRONTEND_OIDC_ENABLED=true
FRONTEND_OIDC_REDIRECT_URI=http://localhost:3000
FRONTEND_OIDC_LOGOUT_REDIRECT_URI=http://localhost:3000
FRONTEND_OIDC_LOGOUT_REDIRECT_URI=http://localhost:3000/login

################################################################################
# Sentry
Expand Down
3 changes: 2 additions & 1 deletion frontend/config/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ export default {
globalSetup: '<rootDir>/config/jest.global.js',
maxWorkers: '50%',
moduleNameMapper: {
'\\.svg\\?react$': '<rootDir>/config/jest.svgImportTransformer.js'
'\\.svg\\?react$': '<rootDir>/config/jest.svgImportTransformer.js',
'\\?worker$': '<rootDir>/config/jest.noopImportTransformer.js'
},
rootDir: '..',
setupFiles: ['dotenv/config', '<rootDir>/config/jest.setup.js'],
Expand Down
5 changes: 5 additions & 0 deletions frontend/config/jest.noopImportTransformer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// eslint-disable-next-line import/no-import-module-exports
import { noop } from 'lodash'

module.exports = noop
module.exports.ReactComponent = noop
17 changes: 17 additions & 0 deletions frontend/cypress/e2e/authorization/authorization.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable no-undef */

context('Authorization', () => {
beforeEach(() => {
cy.loadPath('/#@-824534.42,6082993.21,8.70')
})

it('Should redirect to login page if an API request is Unauthorized', () => {
// When
cy.intercept('GET', `/bff/v1/vessels/search*`, { statusCode: 401 }).as('searchVessel')
cy.get('*[data-cy^="vessel-search-input"]', { timeout: 10000 }).type('Pheno')
cy.wait('@searchVessel')

// Then
cy.location('pathname').should('eq', '/login')
})
})
16 changes: 13 additions & 3 deletions frontend/cypress/e2e/external_monitorfish.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
context('External MonitorFish', () => {
it('Should redirect to /', () => {
// Given
cy.intercept('/bff/v1/authorization/current', { statusCode: 401 }).as('getIsSuperUser')
cy.intercept('/bff/v1/authorization/current', {
body: {
isSuperUser: false
},
statusCode: 200
}).as('getIsSuperUser')
cy.visit('/ext#@-824534.42,6082993.21,8.70')
cy.wait('@getIsSuperUser')

Expand All @@ -10,7 +15,12 @@ context('External MonitorFish', () => {

it('Should have some features removed When not logged as super user', () => {
// Given
cy.intercept('/bff/v1/authorization/current', { statusCode: 401 }).as('getIsSuperUser')
cy.intercept('/bff/v1/authorization/current', {
body: {
isSuperUser: false
},
statusCode: 200
}).as('getIsSuperUser')
cy.visit('/#@-824534.42,6082993.21,8.70')
cy.wait('@getIsSuperUser')
cy.wait(200)
Expand Down Expand Up @@ -70,7 +80,7 @@ context('External MonitorFish', () => {
cy.get('*[data-cy="missions-menu-box"]').should('not.exist')

// Given
cy.loadPath('/ext#@-188008.06,6245230.27,8.70')
cy.loadPath('/#@-188008.06,6245230.27,8.70')

// Then
// No missions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ context('Offline management', () => {
path:
'/bff/v1/vessels/find?vesselId=1&internalReferenceNumber=FAK000999999&externalReferenceNumber=DONTSINK' +
'&IRCS=CALLME&vesselIdentifier=INTERNAL_REFERENCE_NUMBER&trackDepth=TWELVE_HOURS&afterDateTime=&beforeDateTime=',
times: 1
times: 2
},
{ statusCode: 400 }
).as('openVesselStubbed')
Expand Down Expand Up @@ -117,7 +117,7 @@ context('Offline management', () => {
{
method: 'GET',
path: '/bff/v1/vessels/logbook/find?internalReferenceNumber=FAK000999999&voyageRequest=LAST&tripNumber=',
times: 1
times: 2
},
{ statusCode: 400 }
).as('getLogbookStubbed')
Expand All @@ -136,7 +136,7 @@ context('Offline management', () => {
{
method: 'GET',
pathname: '/bff/v1/vessels/reportings',
times: 3
times: 2
},
{ statusCode: 400 }
).as('getReportingsStubbed')
Expand Down
9 changes: 7 additions & 2 deletions frontend/cypress/e2e/nav_monitorfish.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
context('Light MonitorFish', () => {
it('Should have some features removed When not logged as super user', () => {
// Given
cy.intercept('/bff/v1/authorization/current', { statusCode: 401 }).as('getIsSuperUser')
cy.intercept('/bff/v1/authorization/current', {
body: {
isSuperUser: false
},
statusCode: 200
}).as('getIsSuperUser')
cy.visit('/light#@-824534.42,6082993.21,8.70')
cy.wait('@getIsSuperUser')
cy.wait(200)
Expand Down Expand Up @@ -61,7 +66,7 @@ context('Light MonitorFish', () => {
cy.get('*[data-cy="missions-menu-box"]').should('not.exist')

// Given
cy.loadPath('/ext#@-188008.06,6245230.27,8.70')
cy.loadPath('/#@-188008.06,6245230.27,8.70')

// Then
// No missions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ context('Side Window > Mission Form > Main Form', () => {
).as('getMissionStubbed')
editSideWindowMissionListMissionWithId(6, SeafrontGroup.MED)
cy.wait(200)
cy.get('@getMissionStubbed.all').should('have.length', 3)
cy.get('@getMissionStubbed.all').should('have.length', 2)
cy.get('*[data-cy="mission-form-error"]').contains("Nous n'avons pas pu récupérer la mission")
})

Expand Down
10 changes: 9 additions & 1 deletion frontend/cypress/e2e/side_window/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { SideWindowMenuLabel } from '../../../src/domain/entities/sideWindow/constants'

export const openSideWindowAsUser = () => {
cy.intercept('/bff/v1/authorization/current', { statusCode: 401 }).as('getIsSuperUser')
cy.intercept('/bff/v1/authorization/current', {
body: {
isSuperUser: false
},
statusCode: 200
}).as('getIsSuperUser')

cy.viewport(1920, 1080)
cy.visit('/side_window')
Expand All @@ -16,4 +23,5 @@ export const openSideWindowAsSuperUser = () => {
if (document.querySelector('[data-cy="first-loader"]')) {
cy.getDataCy('first-loader').should('not.be.visible')
}
cy.clickButton(SideWindowMenuLabel.PRIOR_NOTIFICATION_LIST)
}
2 changes: 1 addition & 1 deletion frontend/cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Cypress.on('uncaught:exception', err => {

// Run before each spec
beforeEach(() => {
// We use a Cypress session to inject inject a Local Storage key
// We use a Cypress session to inject a Local Storage key
// so that we can detect when the browser app is running in Cypress.
// https://docs.cypress.io/faq/questions/using-cypress-faq#How-do-I-preserve-cookies--localStorage-in-between-my-tests
cy.session('cypress', () => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"dev": "import-meta-env-prepare -x ./.env.local.defaults && vite --port 3000",
"dev-cypress": "FRONTEND_OIDC_ENABLED=false import-meta-env-prepare -u -x ./.env.local.defaults && vite --port 3000",
"dev-monitorenv": "FRONTEND_MONITORENV_URL=//localhost:9880 import-meta-env-prepare -u -x ./.env.local.defaults && vite --port 3000",
"dev-puppeteer": "FRONTEND_MONITORENV_URL=//localhost:9880 import-meta-env-prepare -u -x ./.env.local.defaults && vite --port 3000",
"dev-puppeteer": "FRONTEND_OIDC_ENABLED=false FRONTEND_MONITORENV_URL=//localhost:9880 import-meta-env-prepare -u -x ./.env.local.defaults && vite --port 3000",
"bundle-sw": "esbuild src/workers/serviceWorker.ts --bundle --outfile=public/service-worker.js",
"prepare": "cd .. && ./frontend/node_modules/.bin/husky ./frontend/config/husky",
"generate:testdata": "node ./scripts/generate_test_data_seeds.js",
Expand Down
35 changes: 10 additions & 25 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,33 @@
import { CustomGlobalStyle } from '@components/CustomGlobalStyle'
import { FrontendErrorBoundary } from '@components/FrontendErrorBoundary'
import { GlobalStyle, THEME, ThemeProvider } from '@mtes-mct/monitor-ui'
import { LandingPage } from '@pages/LandingPage'
import { UnsupportedBrowserPage } from '@pages/UnsupportedBrowserPage'
import { isBrowserSupported } from '@utils/isBrowserSupported'
import { UserAccountContext } from 'context/UserAccountContext'
import countries from 'i18n-iso-countries'
import COUNTRIES_FR from 'i18n-iso-countries/langs/fr.json'
import { RouterProvider } from 'react-router-dom'
import { CustomProvider as RsuiteCustomProvider } from 'rsuite'
import rsuiteFrFr from 'rsuite/locales/fr_FR'

import { useCustomAuth } from './auth/hooks/useCustomAuth'
import { router } from './router'

countries.registerLocale(COUNTRIES_FR)

export function App() {
const { isAuthorized, isLoading, userAccount } = useCustomAuth()

if (isLoading) {
return <LandingPage />
}

if (!isAuthorized || !userAccount) {
return <LandingPage hasInsufficientRights />
}

if (!isBrowserSupported()) {
return <UnsupportedBrowserPage />
}

return (
<UserAccountContext.Provider value={userAccount}>
<ThemeProvider theme={THEME}>
<GlobalStyle />
<CustomGlobalStyle />

<RsuiteCustomProvider locale={rsuiteFrFr}>
<FrontendErrorBoundary>
<RouterProvider router={router} />
</FrontendErrorBoundary>
</RsuiteCustomProvider>
</ThemeProvider>
</UserAccountContext.Provider>
<ThemeProvider theme={THEME}>
<GlobalStyle />
<CustomGlobalStyle />

<RsuiteCustomProvider locale={rsuiteFrFr}>
<FrontendErrorBoundary>
<RouterProvider router={router} />
</FrontendErrorBoundary>
</RsuiteCustomProvider>
</ThemeProvider>
)
}
8 changes: 8 additions & 0 deletions frontend/src/api/BackendApi.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ export namespace BackendApi {

export interface ResponseBodyError {
code: ErrorCode | null
data: any
type: ErrorCode | null
}

// Don't forget to mirror any update here in the backend enum.
export enum ErrorCode {
AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED',
EXISTING_MISSION_ACTION = 'EXISTING_MISSION_ACTION',
/** Thrown when attempting to delete an entity which has to non-archived children. */
FOREIGN_KEY_CONSTRAINT = 'FOREIGN_KEY_CONSTRAINT',
Expand All @@ -59,3 +61,9 @@ export namespace BackendApi {
UNARCHIVED_CHILD = 'UNARCHIVED_CHILD'
}
}

export interface Meta {
response?: {
headers: Headers
}
}
27 changes: 14 additions & 13 deletions frontend/src/api/alert.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// TODO We could remove the type discrimation normalization step if we had it done on API side.

import { FrontendApiError } from '@libs/FrontendApiError'

import { monitorfishApi, monitorfishApiKy } from './api'
import { ApiError } from '../libs/ApiError'

import type {
SilencedAlertData,
LEGACY_PendingAlert,
LEGACY_SilencedAlert,
PendingAlert,
SilencedAlert,
SilencedAlertData,
SilencedAlertPeriodRequest
} from '../domain/entities/alerts/types'

Expand Down Expand Up @@ -39,7 +40,7 @@ export const alertApi = monitorfishApi.injectEndpoints({
method: 'POST',
url: `/operational_alerts/silenced`
}),
transformErrorResponse: response => new ApiError(CREATE_SILENCED_ALERT_ERROR_MESSAGE, response)
transformErrorResponse: response => new FrontendApiError(CREATE_SILENCED_ALERT_ERROR_MESSAGE, response)
})
})
})
Expand All @@ -49,35 +50,35 @@ export const { useCreateSilencedAlertMutation } = alertApi
/**
* Get operational alerts
*
* @throws {@link ApiError}
* @throws {@link FrontendApiError}
*/
async function getOperationalAlertsFromAPI(): Promise<LEGACY_PendingAlert[]> {
try {
const data = await monitorfishApiKy.get('/bff/v1/operational_alerts').json<PendingAlert[]>()

return data.map(normalizePendingAlert)
} catch (err) {
throw new ApiError(ALERTS_ERROR_MESSAGE, err)
throw new FrontendApiError(ALERTS_ERROR_MESSAGE, (err as FrontendApiError).originalError)
}
}

/**
* Validate an alert
*
* @throws {@link ApiError}
* @throws {@link FrontendApiError}
*/
async function validateAlertFromAPI(id: string): Promise<void> {
try {
await monitorfishApiKy.put(`/bff/v1/operational_alerts/${id}/validate`)
} catch (err) {
throw new ApiError(VALIDATE_ALERT_ERROR_MESSAGE, err)
throw new FrontendApiError(VALIDATE_ALERT_ERROR_MESSAGE, (err as FrontendApiError).originalError)
}
}

/**
* Silence an alert and returns the saved silenced alert
*
* @throws {@link ApiError}
* @throws {@link FrontendApiError}
*/
async function silenceAlertFromAPI(
id: string,
Expand All @@ -97,33 +98,33 @@ async function silenceAlertFromAPI(
})
.json<SilencedAlert>()
} catch (err) {
throw new ApiError(SILENCE_ALERT_ERROR_MESSAGE, err)
throw new FrontendApiError(SILENCE_ALERT_ERROR_MESSAGE, (err as FrontendApiError).originalError)
}
}

/**
* Get silenced alerts
*
* @throws {@link ApiError}
* @throws {@link FrontendApiError}
*/
async function getSilencedAlertsFromAPI(): Promise<LEGACY_SilencedAlert[]> {
try {
return await monitorfishApiKy.get('/bff/v1/operational_alerts/silenced').json<SilencedAlert[]>()
} catch (err) {
throw new ApiError(ALERTS_ERROR_MESSAGE, err)
throw new FrontendApiError(ALERTS_ERROR_MESSAGE, (err as FrontendApiError).originalError)
}
}

/**
* Delete a silenced alert
*
* @throws {@link ApiError}
* @throws {@link FrontendApiError}
*/
async function deleteSilencedAlertFromAPI(id: string): Promise<void> {
try {
await monitorfishApiKy.delete(`/bff/v1/operational_alerts/silenced/${id}`)
} catch (err) {
throw new ApiError(DELETE_SILENCED_ALERT_ERROR_MESSAGE, err)
throw new FrontendApiError(DELETE_SILENCED_ALERT_ERROR_MESSAGE, (err as FrontendApiError).originalError)
}
}

Expand Down
Loading

0 comments on commit 8ff6e6c

Please sign in to comment.