Skip to content
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
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ NEXT_PUBLIC_SUMMARY_RUN=false
# CRON
PILO_CRON=
SUMMARY_CRON=
USERS_PURGE_CRON=

# Debug
# NEXT_PUBLIC_DEBUG_MODE=false
Expand Down
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "medle",
"version": "1.38.0",
"version": "1.39.0",
"private": true,
"engines": {
"node": ">=20.9.0"
Expand Down
49 changes: 36 additions & 13 deletions src/__tests__/unit/pages/forgot-password.spec.js
Original file line number Diff line number Diff line change
@@ -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 }

Expand All @@ -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 = {
Expand All @@ -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
})
Expand Down
67 changes: 42 additions & 25 deletions src/__tests__/unit/pages/reset-password.spec.js
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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 }

Expand All @@ -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 = {
Expand All @@ -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
})
Expand Down
22 changes: 22 additions & 0 deletions src/clients/app-settings.js
Original file line number Diff line number Diff line change
@@ -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)
}
103 changes: 31 additions & 72 deletions src/components/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -417,87 +417,46 @@ 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 (
<Link
key={pageKey}
href={href}
className={getAdminItemClass(pageKey, page)}
id={pageKey === "users" ? "adminNavigation" : undefined}
aria-current={page === pageKey ? "true" : "false"}
>
<Icon width={30} /> <br />
{label}
</Link>
)
}

const SidebarAdmin = ({ page, currentUser }) => {
if (!currentUser) return ""

const isSuperAdmin = currentUser.role === SUPER_ADMIN
const isAdmin = isAllowed(currentUser.role, ADMIN)

return (
<>
<nav
role="navigation"
aria-label="Navigation latérale d'administration"
className="text-center list-group list-group-flush"
>
{isAllowed(currentUser.role, ADMIN) && (
<Link
href="/administration/users"
className={"list-group-item list-group-item-action " + (page === "users" ? "selected" : "unselected")}
id="adminNavigation"
aria-current={page === "users" ? "true" : "false"}
>
<FaceIcon width={30} />
<br />
Utilisateurs
</Link>
)}
{currentUser.role === SUPER_ADMIN && (
<Link
href="/administration/hospitals"
className={"list-group-item list-group-item-action " + (page === "hospitals" ? "selected" : "unselected")}
aria-current={page === "hospitals" ? "true" : "false"}
>
<ApartmentIcon width={30} /> <br />
Établissements
</Link>
)}
{currentUser.role === SUPER_ADMIN && (
<Link
href="/administration/askers"
className={"list-group-item list-group-item-action " + (page === "askers" ? "selected" : "unselected")}
aria-current={page === "askers" ? "true" : "false"}
>
<AccountBalanceIcon width={30} /> <br />
Demandeurs
</Link>
)}
{currentUser.role === SUPER_ADMIN && (
<Link
href="/administration/attacks"
className={"list-group-item list-group-item-action " + (page === "attacks" ? "selected" : "unselected")}
aria-current={page === "attacks" ? "true" : "false"}
>
<WhatshotIcon width={30} /> <br />
Attentats
</Link>
)}
{currentUser.role === SUPER_ADMIN && (
<Link
href="/administration/acts"
className={"list-group-item list-group-item-action " + (page === "acts" ? "selected" : "unselected")}
aria-current={page === "acts" ? "true" : "false"}
>
<ReceiptIcon width={30} /> <br />
Actes
</Link>
)}
{currentUser.role === SUPER_ADMIN && (
<Link
href="/administration/messages"
className={"list-group-item list-group-item-action " + (page === "messages" ? "selected" : "unselected")}
aria-current={page === "messages" ? "true" : "false"}
>
<AnnouncementIcon width={30} /> <br />
Messages
</Link>
)}
{currentUser.role === SUPER_ADMIN && (
<Link
href="/administration/logos"
className={"list-group-item list-group-item-action " + (page === "logos" ? "selected" : "unselected")}
aria-current={page === "logos" ? "true" : "false"}
>
<ImageIcon width={30} /> <br />
Logos
</Link>
)}
{renderAdminLink("/administration/users", "Utilisateurs", FaceIcon, "users", page, isAdmin)}
{renderAdminLink("/administration/hospitals", "Établissements", ApartmentIcon, "hospitals", page, isSuperAdmin)}
{renderAdminLink("/administration/askers", "Demandeurs", AccountBalanceIcon, "askers", page, isSuperAdmin)}
{renderAdminLink("/administration/attacks", "Attentats", WhatshotIcon, "attacks", page, isSuperAdmin)}
{renderAdminLink("/administration/acts", "Actes", ReceiptIcon, "acts", page, isSuperAdmin)}
{renderAdminLink("/administration/messages", "Messages", AnnouncementIcon, "messages", page, isSuperAdmin)}
{renderAdminLink("/administration/logos", "Logos", ImageIcon, "logos", page, isSuperAdmin)}
{renderAdminLink("/administration/settings", "Paramètres", SettingsIcon, "settings", page, isSuperAdmin)}
<Link href={startPageForRole(currentUser.role)} className="list-group-item list-group-item-action">
<ArrowBackIcon width={30} /> <br />
Retour
Expand Down
1 change: 1 addition & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export const EMPLOYMENTS_ENDPOINT = "/employments"
export const ATTACKS_ENDPOINT = "/attacks"
export const HOSPITALS_ENDPOINT = "/hospitals"
export const USERS_ENDPOINT = "/users"
export const APP_SETTINGS_ENDPOINT = "/app-settings"

export const GLOBAL_STATISTICS_ENDPOINT = "/statistics/global"
export const LIVING_STATISTICS_ENDPOINT = "/statistics/living"
Expand Down
Loading
Loading