diff --git a/.env.sample b/.env.sample index 8eb9fdc..2cbd580 100644 --- a/.env.sample +++ b/.env.sample @@ -44,6 +44,7 @@ NEXT_PUBLIC_SUMMARY_RUN=false # CRON PILO_CRON= SUMMARY_CRON= +USERS_PURGE_CRON= # Debug # NEXT_PUBLIC_DEBUG_MODE=false diff --git a/CHANGELOG.md b/CHANGELOG.md index 27e5822..2644437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ -## [1.39.0](https://github.com/DNUM-SocialGouv/Medle/compare/release-1.38.0...release-1.39.0) (TODO) +## [1.40.0](https://github.com/DNUM-SocialGouv/Medle/compare/release-1.39.0...release-1.40.0) (TODO) ### Feature +## [1.39.0](https://github.com/DNUM-SocialGouv/Medle/compare/release-1.38.0...release-1.39.0) (2026-08-26) +### Feature + +* MED-112: Correction retours page FAQ +* MED-110: Prévoir un système de purge des utilisateurs + + ## [1.38.0](https://github.com/DNUM-SocialGouv/Medle/compare/release-1.37.0...release-1.38.0) (2026-07-31) ### Feature diff --git a/package.json b/package.json index a761de4..d51cb3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "medle", - "version": "1.38.0", + "version": "1.39.0", "private": true, "engines": { "node": ">=20.9.0" diff --git a/src/__tests__/unit/pages/forgot-password.spec.js b/src/__tests__/unit/pages/forgot-password.spec.js index 99e5631..9fce46c 100644 --- a/src/__tests__/unit/pages/forgot-password.spec.js +++ b/src/__tests__/unit/pages/forgot-password.spec.js @@ -1,13 +1,15 @@ import { render, screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" import faker from "faker" -import { rest } from "msw" -import { setupServer } from "msw/node" import React from "react" import { API_URL, FORGOT_PWD_ENDPOINT } from "../../../config" import ForgotPasswordPage from "../../../pages/forgot-password" +// Mock isomorphic-unfetch before importing components +jest.mock("isomorphic-unfetch") +import fetch from "isomorphic-unfetch" + const originalWindow = { ...window } const originalConsoleError = { ...console.error } @@ -16,17 +18,40 @@ const foundEmail = "xx" + notFoundEmail // Ensure to have consistently a differe const url = `${API_URL}${FORGOT_PWD_ENDPOINT}` -const server = setupServer( - rest.post(url, (req, res, ctx) => { - if (req.body?.email === notFoundEmail) { - return res(ctx.status(404), ctx.json({ message: `User with email ${notFoundEmail} doesn't exist.`, status: 404 })) +// Setup fetch mock +fetch.mockImplementation((fetchUrl, options) => { + if (fetchUrl === url && options?.method === "POST") { + let body = {} + try { + body = typeof options.body === 'string' ? JSON.parse(options.body) : options.body + } catch (e) { + // If body parsing fails, treat as empty + } + + if (body?.email === notFoundEmail) { + return Promise.resolve({ + ok: false, + status: 404, + json: () => Promise.resolve({ message: `User with email ${notFoundEmail} doesn't exist.`, status: 404 }), + }) } - return res(ctx.status(200), ctx.json({})) - }), -) + + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({}), + }) + } + + // Default response for other requests + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({}), + }) +}) beforeAll(() => { - server.listen() // Disable window._paq.push used by Matomo. if (!window?._paq?.push) { window._paq = { @@ -37,13 +62,11 @@ beforeAll(() => { }) afterEach(() => { - server.resetHandlers() jest.clearAllMocks() }) afterAll(() => { - server.close() - // eslint-disable-next-line no-global-assign + window = originalWindow console.error = originalConsoleError }) diff --git a/src/__tests__/unit/pages/reset-password.spec.js b/src/__tests__/unit/pages/reset-password.spec.js index b3b2842..c6946a2 100644 --- a/src/__tests__/unit/pages/reset-password.spec.js +++ b/src/__tests__/unit/pages/reset-password.spec.js @@ -1,8 +1,6 @@ import { render, screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" import faker from "faker" -import { rest } from "msw" -import { setupServer } from "msw/node" import * as nextRouter from "next/router" import React from "react" @@ -11,6 +9,10 @@ import ResetPasswordPage from "../../../pages/reset-password" import { generateToken } from "../../../utils/jwt" import { mockRouterImplementation } from "../../../utils/test-utils" +// Mock isomorphic-unfetch before importing components +jest.mock("isomorphic-unfetch") +import fetch from "isomorphic-unfetch" + const originalWindow = { ...window } const originalConsoleError = { ...console.error } @@ -20,31 +22,47 @@ const user = { email: faker.internet.email() } const correctLoginToken = generateToken(user, { timeout: "1H" }) const incorrectLoginToken = generateToken(user, { timeout: "100ms" }) // Token only valid for 100 ms. -beforeAll(() => { - /* eslint-disable no-import-assign*/ - nextRouter.useRouter.mockImplementation(() => ({ - ...mockRouterImplementation, - query: { loginToken: correctLoginToken }, - })) -}) - -afterAll(() => { - nextRouter.useRouter.mockRestore() -}) - const url = `${API_URL}${RESET_PWD_ENDPOINT}` -const server = setupServer( - rest.patch(url, (req, res, ctx) => { - if (req.body?.loginToken === correctLoginToken) { - return res(ctx.status(200), ctx.json({})) +// Setup fetch mock +fetch.mockImplementation((fetchUrl, options) => { + if (fetchUrl === url && options?.method === "PATCH") { + let body = {} + try { + body = typeof options.body === 'string' ? JSON.parse(options.body) : options.body + } catch (e) { + // If body parsing fails, treat as empty + } + + if (body?.loginToken === correctLoginToken) { + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({}), + }) } - return res(ctx.status(500)) - }), -) + + return Promise.resolve({ + ok: false, + status: 500, + json: () => Promise.resolve({}), + }) + } + + // Default response for other requests + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({}), + }) +}) beforeAll(() => { - server.listen() + + nextRouter.useRouter.mockImplementation(() => ({ + ...mockRouterImplementation, + query: { loginToken: correctLoginToken }, + })) // Disable window._paq.push used by Matomo. if (!window?._paq?.push) { window._paq = { @@ -55,13 +73,12 @@ beforeAll(() => { }) afterEach(() => { - server.resetHandlers() jest.clearAllMocks() }) afterAll(() => { - server.close() - // eslint-disable-next-line no-global-assign + nextRouter.useRouter.mockRestore() + window = originalWindow console.error = originalConsoleError }) diff --git a/src/clients/app-settings.js b/src/clients/app-settings.js new file mode 100644 index 0000000..eaea6c1 --- /dev/null +++ b/src/clients/app-settings.js @@ -0,0 +1,22 @@ +import fetch from "isomorphic-unfetch" + +import { API_URL, APP_SETTINGS_ENDPOINT } from "../config" +import { handleAPIResponse2 } from "../utils/errors" +import { METHOD_PUT } from "../utils/http" + +const appSettingsEndpoint = API_URL + APP_SETTINGS_ENDPOINT + +export const findAppSettings = async (headers = {}) => { + const response = await fetch(appSettingsEndpoint, { headers }) + return handleAPIResponse2(response) +} + +export const updateAppSettings = async ({ usersPurgeInactivityDays, headers = {} }) => { + const response = await fetch(appSettingsEndpoint, { + body: JSON.stringify({ usersPurgeInactivityDays }), + headers: { ...headers, "Content-Type": "application/json" }, + method: METHOD_PUT, + }) + + return handleAPIResponse2(response) +} \ No newline at end of file diff --git a/src/components/Layout.js b/src/components/Layout.js index 95d60f1..83eafe9 100644 --- a/src/components/Layout.js +++ b/src/components/Layout.js @@ -417,8 +417,31 @@ Sidebar.propTypes = { page: PropTypes.string, } +const getAdminItemClass = (itemPage, currentPage) => + `list-group-item list-group-item-action ${currentPage === itemPage ? "selected" : "unselected"}` + +const renderAdminLink = (href, label, Icon, pageKey, page, isVisible) => { + if (!isVisible) return null + return ( + +
+ {label} + + ) +} + const SidebarAdmin = ({ page, currentUser }) => { if (!currentUser) return "" + + const isSuperAdmin = currentUser.role === SUPER_ADMIN + const isAdmin = isAllowed(currentUser.role, ADMIN) + return ( <>