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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ docker/auth/*.local
# npm / pnpm
node_modules
.pnpm-store
# npm cache lands here because the frontend Dockerfile sets
# NPM_CONFIG_CACHE=/home/frontend/.npm (so npm can write a cache without
# needing /.npm/_logs at the root). When the host bind-mounts
# ./src/frontend onto /home/frontend, the cache ends up on the host.
.npm/

# Mails
src/backend/core/templates/mail/
Expand Down
17 changes: 17 additions & 0 deletions src/frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ COPY ./apps/calendars ./apps/calendars

WORKDIR /home/frontend

# Make the image and any anonymous volumes derived from it writable by
# any host user whose group ID is `node` (1000). Compose runs the
# container as `${DOCKER_USER:-1000}` which resolves to the host
# uid:gid — on macOS that's typically 501:20 / 501:1000, on Linux often
# 1000:1000. Owning everything by `node:node` plus `g+w` lets npm
# rename/rewrite packages from either case, so `make install-front`
# Just Works without a root-owned anonymous-volume fight.
#
# Also pre-create the npm cache + log directories under HOME so npm
# doesn't try to write to `/.npm/_logs` (which it can't, as a non-root
# user with no $HOME).
RUN mkdir -p /home/frontend/.npm \
&& chown -R node:node /home/frontend \
&& chmod -R g+w /home/frontend
ENV HOME=/home/frontend
ENV NPM_CONFIG_CACHE=/home/frontend/.npm

### ---- Front-end builder image ----
FROM frontend-deps AS calendars

Expand Down
13 changes: 2 additions & 11 deletions src/frontend/apps/calendars/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,28 @@
"@gouvfr-lasuite/cunningham-react": "4.2.0",
"@gouvfr-lasuite/ui-kit": "0.19.10",
"@tanstack/react-query": "5.90.21",
"@tanstack/react-table": "8.21.3",
"@viselect/react": "3.9.0",
"clsx": "2.1.1",
"date-fns": "4.1.0",
"i18next": "25.8.14",
"i18next-browser-languagedetector": "8.2.1",
"ical.js": "2.2.1",
"next": "16.1.6",
"next-i18next": "15.4.3",
"pretty-bytes": "7.1.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-dropzone": "15.0.0",
"react-hook-form": "7.71.2",
"react-i18next": "16.5.6",
"react-toastify": "11.0.5",
"sass": "1.97.3",
"ts-ics": "2.4.3",
"tsdav": "2.1.8"
"ts-ics": "2.4.3"
},
"devDependencies": {
"@gouvfr-lasuite/cunningham-tokens": "3.1.0",
"@tanstack/eslint-plugin-query": "5.91.4",
"@tanstack/react-query-devtools": "5.91.3",
"@types/jest": "30.0.0",
"@types/node": "24.12.0",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"eslint": "9.30.1",
"eslint-config-next": "16.1.6",
"jest": "30.2.0",
"jest-environment-jsdom": "30.2.0",
"ts-jest": "29.4.6",
"typescript": "5.9.3"
}
Expand Down
26 changes: 22 additions & 4 deletions src/frontend/apps/calendars/src/features/api/fetchApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,30 @@ export const SESSION_STORAGE_REDIRECT_AFTER_LOGIN_URL =
/**
* Redirect to the login page, saving the current URL for post-login redirect.
* Called on any 401 response to handle expired sessions.
*
* Idempotent — if several concurrent requests all return 401 (e.g. multiple
* React Query refetches on window focus), only the first call actually
* persists the URL and triggers navigation; later calls are no-ops so the
* stored `redirect_after_login_url` doesn't get overwritten by whatever
* the URL happens to be mid-navigation.
*/
let redirectInFlight = false;
export function redirectToLogin() {
sessionStorage.setItem(
SESSION_STORAGE_REDIRECT_AFTER_LOGIN_URL,
window.location.href,
);
if (redirectInFlight) return;
redirectInFlight = true;
// Persisting the current URL is best-effort: Safari Private Browsing,
// a full storage quota, or a `SecurityError` from a sandboxed context
// can all throw here. Treat that as "we just won't restore the URL
// after login" — never let it suppress the actual navigation, which
// is what unsticks the session.
try {
sessionStorage.setItem(
SESSION_STORAGE_REDIRECT_AFTER_LOGIN_URL,
window.location.href,
);
} catch {
// intentionally swallow — the redirect itself is what matters.
}
window.location.replace(new URL("authenticate/", baseApiUrl()).href);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { useTranslation } from "react-i18next";
import { CalDavService } from "../services/dav/CalDavService";
import { EventCalendarAdapter } from "../services/dav/EventCalendarAdapter";
import { caldavServerUrl, headers, fetchOptions } from "../utils/DavClient";
import { caldavServerUrl } from "../utils/DavClient";
import type {
CalDavCalendar,
CalDavCalendarCreate,
Expand Down Expand Up @@ -415,8 +415,6 @@ export const CalendarContextProvider = ({
try {
const result = await caldavService.connect({
serverUrl: caldavServerUrl,
headers,
fetchOptions,
userEmail,
});
if (isMounted && result.success) {
Expand Down
Loading
Loading