Author SHA1 Message Date
crisleo94 a6c14b52fd Merge pull request 'fix: Auth error handling' (#15) from fix/user-auth-error-handling into main
Reviewed-on: #15
2026-05-29 02:25:58 +00:00
Marko (Hermes Implementer) 2e71eb73b8 Merge branch 'main' into fix/user-auth-error-handling
Resolved conflicts in:
  - api/accounts/urls.py: Took main's complete URL set (verify/refresh/logout/me)
  - web/src/App.tsx: Took main's route structure with PublicRoute/ProtectedRoute wrappers
  - web/src/services/authApi.ts: Combined main's robust interceptor with PR's extractErrorMessage + getAccessToken/getRefreshToken helpers
  - web/src/contexts/AuthContext.tsx: Combined main's full auth flow (refreshAccessToken, getProfile, logoutUser) with PR's extractErrorMessage and user_data persistence
2026-05-29 02:24:51 +00:00
crisleo94 7dde10b7ec Merge pull request 'Implement: Robust API Security Measures (Phase 1)' (#14) from feature/api-security-phase-1 into main
Reviewed-on: #14
2026-05-29 02:20:05 +00:00
Marko (Hermes Implementer) 84c6262b3d fix: proper Axios error extraction, token refresh, auth state restoration, ProtectedRoute 2026-05-29 00:13:31 +00:00
5 changed files with 379 additions and 40 deletions
+37 -1
View File
@@ -221,4 +221,40 @@ Axios-based HTTP client with:
- Inactive accounts rejected at login time
- User emails normalized to lowercase before storage
- JWT tokens stored in localStorage (trade-off: simple SPA integration vs XSS exposure — refresh tokens mitigate short exposure)
- `AUTH_HEADER_TYPES` set to `("Bearer",)` following RFC 6750
- `AUTH_HEADER_TYPES` set to `("Bearer",)` following RFC 6750
---
## Post-Merge Fixes (PR #14)
**Issue:** Issue #3 was re-opened after initial merge. Root cause: frontend error handling was using `err.message` from AxiosError objects, which produces `"Request failed with status code 400"` instead of the actual backend validation errors.
### Changes
| Area | Change | Detail |
|---|---|---|
| `api/accounts/urls.py` | Added `token/refresh/` endpoint | Maps to `rest_framework_simplejwt.views.TokenRefreshView` |
| `web/src/services/authApi.ts` | **Error extraction** | New `extractErrorMessage()` function parses `err.response.data` — handles `non_field_errors`, `detail`, and field-level error arrays |
| `web/src/services/authApi.ts` | **Token refresh interceptor** | Axios response interceptor intercepts 401, refreshes access token using stored refresh_token, replays queued requests |
| `web/src/services/authApi.ts` | Export `setTokens`, `clearTokens`, `getAccessToken` | Shared token utility functions for AuthContext |
| `web/src/contexts/AuthContext.tsx` | **Fixed error handling** | Uses `extractErrorMessage(err)` instead of `err instanceof Error ? err.message : ...` |
| `web/src/contexts/AuthContext.tsx` | **Auth state restoration** | `RESTORE_COMPLETE` action; `isRestoring` flag prevents protected route flash on page refresh |
| `web/src/contexts/AuthContext.tsx` | **Persist user data** | Stores `user_data` in localStorage alongside tokens; restores `UserProfile` on page reload |
| `web/src/components/ProtectedRoute.tsx` | New component | Route guard — redirects to `/login` if unauthenticated; shows loading spinner during restoration |
| `web/src/App.tsx` | Route restructure | Public routes (`/login`, `/register`) outside ProtectedRoute; protected routes (`/`) inside it |
### Error handling flow
1. Backend returns `{"email": ["A user with this email already exists."]}` (DRF standard)
2. Axios interceptor catches non-401 responses, passes error through
3. `extractErrorMessage()` iterates `response.data` keys, returns first error string
4. AuthContext dispatches `AUTH_FAILURE` with extracted message
5. LoginPage/RegisterPage renders `state.error` — user sees: "A user with this email already exists."
### Token refresh flow
1. User logs in — `access_token` and `refresh_token` stored in localStorage
2. When access token expires, next API call receives 401
3. Response interceptor catches it, calls `POST /api/auth/token/refresh/`
4. On success — new access token stored, original request retried, queued requests replayed
5. On failure — tokens cleared, all pending requests rejected with "Session expired"
+49
View File
@@ -0,0 +1,49 @@
import { type ReactNode } from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import CircularProgress from "@mui/material/CircularProgress";
import Box from "@mui/material/Box";
interface ProtectedRouteProps {
/** Optional redirect path for unauthenticated users (default: /login) */
redirectTo?: string;
/** Optional fallback UI while restoring auth state */
fallback?: ReactNode;
}
/**
* Route guard that redirects unauthenticated users to the login page.
* Shows a loading spinner while auth state is being restored from localStorage.
*/
export default function ProtectedRoute({
redirectTo = "/login",
fallback,
}: ProtectedRouteProps): ReactNode {
const { state } = useAuth();
const location = useLocation();
// Still checking localStorage for existing session
if (state.isRestoring) {
return (
fallback ?? (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
}}
>
<CircularProgress />
</Box>
)
);
}
if (!state.isAuthenticated) {
// Preserve the attempted URL so we can redirect back after login
return <Navigate to={redirectTo} state={{ from: location }} replace />;
}
return <Outlet />;
}
+24 -6
View File
@@ -15,6 +15,8 @@ import {
setTokens,
clearTokens,
refreshAccessToken,
extractErrorMessage,
getAccessToken,
type UserProfile,
type LoginPayload,
type RegisterPayload,
@@ -63,6 +65,7 @@ function authReducer(state: AuthState, action: AuthAction): AuthState {
return {
...state,
isLoading: false,
isInitializing: false,
error: action.payload,
};
case "LOGOUT":
@@ -97,6 +100,20 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const initAuth = async () => {
const refreshToken = localStorage.getItem("refresh_token");
if (!refreshToken) {
// Still check for legacy access_token to restore from localStorage
const accessToken = getAccessToken();
if (accessToken) {
const userData = localStorage.getItem("user_data");
if (userData) {
try {
const user: UserProfile = JSON.parse(userData);
dispatch({ type: "AUTH_SUCCESS", payload: user });
return;
} catch {
// corrupt stored data — proceed with unauthenticated state
}
}
}
dispatch({ type: "INIT_COMPLETE" });
return;
}
@@ -108,10 +125,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
// Fetch user profile with the fresh token
const profile = await getProfile();
// Persist user data for restoration if refresh token expires
localStorage.setItem("user_data", JSON.stringify(profile));
dispatch({ type: "AUTH_SUCCESS", payload: profile });
} catch {
// Token invalid or expired — clear everything
clearTokens();
localStorage.removeItem("user_data");
dispatch({ type: "LOGOUT" });
}
};
@@ -124,10 +144,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
try {
const response = await loginUser(payload);
setTokens(response.access, response.refresh);
localStorage.setItem("user_data", JSON.stringify(response.user));
dispatch({ type: "AUTH_SUCCESS", payload: response.user });
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : "Login failed. Please try again.";
const message = extractErrorMessage(err);
dispatch({ type: "AUTH_FAILURE", payload: message });
throw err;
}
@@ -140,10 +160,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
// Registration succeeded — the page component handles redirect to /login.
dispatch({ type: "LOGOUT" });
} catch (err: unknown) {
const message =
err instanceof Error
? err.message
: "Registration failed. Please try again.";
const message = extractErrorMessage(err);
dispatch({ type: "AUTH_FAILURE", payload: message });
throw err;
}
@@ -159,6 +176,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}
}
clearTokens();
localStorage.removeItem("user_data");
dispatch({ type: "LOGOUT" });
}, []);
+101 -32
View File
@@ -1,4 +1,8 @@
import axios from "axios";
import axios, { AxiosError, type AxiosResponse, type InternalAxiosRequestConfig } from "axios";
interface RetryConfig extends InternalAxiosRequestConfig {
_retry?: boolean;
}
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_URL || "http://localhost:8000",
@@ -7,16 +11,38 @@ const apiClient = axios.create({
},
});
// Attach access token to every request if present
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem("access_token");
// ── Token management ──────────────────────────────────────────────────
function getAccessToken(): string | null {
return localStorage.getItem("access_token");
}
function getRefreshToken(): string | null {
return localStorage.getItem("refresh_token");
}
function setTokens(access: string, refresh: string): void {
localStorage.setItem("access_token", access);
localStorage.setItem("refresh_token", refresh);
}
function clearTokens(): void {
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
}
// ── Request interceptor: attach access token ──────────────────────────
apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const token = getAccessToken();
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor: auto-refresh on 401, retry once
// ── Response interceptor: auto-refresh on 401, retry once ─────────────
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string) => void;
@@ -35,11 +61,15 @@ function processQueue(error: unknown, token: string | null = null): void {
}
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
(response: AxiosResponse) => response,
async (error: AxiosError) => {
const originalRequest = error.config as RetryConfig;
// Only handle 401s that aren't already refresh/login/register attempts
if (!originalRequest) {
return Promise.reject(error);
}
// Only handle 401s that aren't already refresh/login/register/logout attempts
if (
error.response?.status !== 401 ||
originalRequest._retry ||
@@ -56,7 +86,7 @@ apiClient.interceptors.response.use(
return new Promise<string>((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
originalRequest.headers!.Authorization = `Bearer ${token}`;
return apiClient(originalRequest);
});
}
@@ -64,39 +94,33 @@ apiClient.interceptors.response.use(
originalRequest._retry = true;
isRefreshing = true;
const refreshToken = localStorage.getItem("refresh_token");
const refreshToken = getRefreshToken();
if (!refreshToken) {
isRefreshing = false;
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
// Redirect to login
clearTokens();
window.location.href = "/login";
return Promise.reject(error);
}
try {
const response = await axios.post(
`${
import.meta.env.VITE_API_URL || "http://localhost:8000"
}/api/auth/token/refresh/`,
`${apiClient.defaults.baseURL}/api/auth/token/refresh/`,
{ refresh: refreshToken }
);
const newAccessToken = response.data.access;
const newRefreshToken = response.data.refresh;
localStorage.setItem("access_token", newAccessToken);
localStorage.setItem("refresh_token", newRefreshToken);
setTokens(newAccessToken, newRefreshToken);
processQueue(null, newAccessToken);
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
originalRequest.headers!.Authorization = `Bearer ${newAccessToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
clearTokens();
window.location.href = "/login";
return Promise.reject(refreshError);
} finally {
@@ -105,6 +129,53 @@ apiClient.interceptors.response.use(
}
);
// ── Error extraction from Axios responses ─────────────────────────────
/**
* Extract a human-readable error message from an AxiosError.
* Backend DRF errors can be:
* - {"field": ["message"]} (field-level)
* - {"non_field_errors": ["message"]} (general)
* - {"detail": "message"} (list/generic)
* - string (unexpected shape)
*/
export function extractErrorMessage(err: unknown): string {
if (err instanceof AxiosError && err.response?.data) {
const data = err.response.data as Record<string, unknown>;
// DRF non_field_errors
if (
Array.isArray(data.non_field_errors) &&
data.non_field_errors.length > 0
) {
return String(data.non_field_errors[0]);
}
// DRF detail (e.g., 401 Unauthorized)
if (typeof data.detail === "string") {
return data.detail;
}
// Field-level errors — pick the first one
for (const key of Object.keys(data)) {
const val = data[key];
if (Array.isArray(val) && val.length > 0) {
return String(val[0]);
}
if (typeof val === "string") {
return val;
}
}
// Fallback: raw string
if (typeof data === "string") return data;
}
// Generic error fallback
if (err instanceof Error) return err.message;
return "An unexpected error occurred. Please try again.";
}
// ── Types ──────────────────────────────────────────────────────────────
export interface RegisterPayload {
@@ -132,7 +203,7 @@ export interface LoginResponse {
refresh: string;
}
// ── API Functions ──────────────────────────────────────────────────────
// ── API functions ──────────────────────────────────────────────────────
export function registerUser(payload: RegisterPayload): Promise<UserProfile> {
return apiClient
@@ -169,12 +240,10 @@ export function refreshAccessToken(
.then((res) => res.data);
}
export function setTokens(access: string, refresh: string): void {
localStorage.setItem("access_token", access);
localStorage.setItem("refresh_token", refresh);
}
export function clearTokens(): void {
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
}
export {
apiClient,
getAccessToken,
getRefreshToken,
setTokens,
clearTokens,
};
+168 -1
View File
@@ -732,6 +732,28 @@
"@types/babel__core" "^7.20.5"
react-refresh "^0.17.0"
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
axios@^1.7.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.16.1.tgz#517e29291d19d6e8cf919ff264f4fe157261ba12"
integrity sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==
dependencies:
follow-redirects "^1.16.0"
form-data "^4.0.5"
https-proxy-agent "^5.0.1"
proxy-from-env "^2.1.0"
babel-plugin-macros@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1"
@@ -757,6 +779,14 @@ browserslist@^4.24.0:
node-releases "^2.0.36"
update-browserslist-db "^1.2.3"
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@@ -772,6 +802,13 @@ clsx@^2.1.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
convert-source-map@^1.5.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
@@ -803,13 +840,18 @@ csstype@^3.0.2, csstype@^3.2.2, csstype@^3.2.3:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
debug@^4.1.0, debug@^4.3.1:
debug@4, debug@^4.1.0, debug@^4.3.1:
version "4.4.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
dependencies:
ms "^2.1.3"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
dom-helpers@^5.0.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
@@ -818,6 +860,15 @@ dom-helpers@^5.0.1:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
dependencies:
call-bind-apply-helpers "^1.0.1"
es-errors "^1.3.0"
gopd "^1.2.0"
electron-to-chromium@^1.5.328:
version "1.5.361"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz#b993bc7b34ea83f348aa1787a608ecf12e39b909"
@@ -830,11 +881,33 @@ error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
es-define-property@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
es-errors@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz#a2d0b373205724dfa525d23b0c3e1b1ca582c99b"
integrity sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==
dependencies:
es-errors "^1.3.0"
es-set-tostringtag@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
dependencies:
es-errors "^1.3.0"
get-intrinsic "^1.2.6"
has-tostringtag "^1.0.2"
hasown "^2.0.2"
esbuild@^0.25.0:
version "0.25.12"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.12.tgz#97a1d041f4ab00c2fce2f838d2b9969a2d2a97a5"
@@ -887,6 +960,22 @@ find-root@^1.1.0:
resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
follow-redirects@^1.16.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc"
integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==
form-data@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053"
integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
hasown "^2.0.2"
mime-types "^2.1.12"
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
@@ -902,6 +991,54 @@ gensync@^1.0.0-beta.2:
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
get-intrinsic@^1.2.6:
version "1.3.0"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
dependencies:
call-bind-apply-helpers "^1.0.2"
es-define-property "^1.0.1"
es-errors "^1.3.0"
es-object-atoms "^1.1.1"
function-bind "^1.1.2"
get-proto "^1.0.1"
gopd "^1.2.0"
has-symbols "^1.1.0"
hasown "^2.0.2"
math-intrinsics "^1.1.0"
get-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
dependencies:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
gopd@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
has-symbols@^1.0.3, has-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
has-tostringtag@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
dependencies:
has-symbols "^1.0.3"
hasown@^2.0.2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003"
integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==
dependencies:
function-bind "^1.1.2"
hasown@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.3.tgz#5e5c2b15b60370a4c7930c383dfb76bf17bc403c"
@@ -916,6 +1053,14 @@ hoist-non-react-statics@^3.3.1:
dependencies:
react-is "^16.7.0"
https-proxy-agent@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
dependencies:
agent-base "6"
debug "4"
import-fresh@^3.2.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf"
@@ -975,6 +1120,23 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
ms@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
@@ -1050,6 +1212,11 @@ prop-types@^15.6.2, prop-types@^15.8.1:
object-assign "^4.1.1"
react-is "^16.13.1"
proxy-from-env@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba"
integrity sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==
react-dom@^19.0.0:
version "19.2.6"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.6.tgz#44a81b0bcca22da814c00847d09d01c8615529b7"