Skip to content
Open
Show file tree
Hide file tree
Changes from 144 commits
Commits
Show all changes
153 commits
Select commit Hold shift + click to select a range
11519b2
Introduce centralized authentication context to manage auth state and…
Ben-El Oct 21, 2025
9ef2f9d
Refactor `useUser` and `ConfigProvider` hooks to use `useCallback` an…
Ben-El Oct 22, 2025
69a0025
Consolidate authentication status and logic into `authContext`, remov…
Ben-El Oct 23, 2025
626debf
Refactor authentication logic by consolidating `markAuthenticated` an…
Ben-El Oct 23, 2025
4bca6ab
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Oct 23, 2025
4796e8b
Refactor login flow to use `RedirectIfAuthenticated` component, centr…
Ben-El Oct 23, 2025
d8368f3
Refactor `RedirectIfAuthenticated` to use a functional component and …
Ben-El Oct 23, 2025
0ff7e8d
Refactor `authContext` to use functional components, arrow functions,…
Ben-El Oct 23, 2025
3352594
Refactor `RequiresAuth` to use `React.FC` and arrow function for cons…
Ben-El Oct 23, 2025
95b9d5a
Refactor `authContext` to reorder `AuthContext` and `AuthContextType`…
Ben-El Oct 23, 2025
5d881bc
Refactor login flow to handle redirect authentication with `useEffect…
Ben-El Oct 26, 2025
ca2f40d
Refactor `login.tsx` to simplify `next` variable assignment by removi…
Ben-El Oct 26, 2025
46038ea
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Oct 26, 2025
6f930a2
Refactor `login.tsx` to reorder conditional logic for readability and…
Ben-El Oct 26, 2025
e3a698d
Refactor `login.tsx` to replace `useEffect` with direct conditional n…
Ben-El Oct 26, 2025
a61eb21
Refactor `login.tsx` to remove unused `useEffect` import.
Ben-El Oct 26, 2025
b8260b5
Refactor `login.tsx` to remove redundant type assertions in `next` va…
Ben-El Oct 26, 2025
f33070a
Refactor `login.tsx` to remove unused authenticated redirect logic fo…
Ben-El Oct 26, 2025
5cd834e
Refactor `login.tsx` to remove unused imports and redundant `useAuth`…
Ben-El Oct 26, 2025
2ced21e
Refactor authentication flow in `login.tsx` and `requiresAuth.tsx` to…
Ben-El Oct 26, 2025
c13ade2
Refactor `login.tsx` to simplify `next` variable assignment by removi…
Ben-El Oct 26, 2025
2cfcb92
Refactor `AuthProvider` to use `useEffect` for dynamic authentication…
Ben-El Oct 26, 2025
1cbbcf2
Refactor `api.jsx` to reset request state on authentication error for…
Ben-El Oct 26, 2025
13a8f05
Refactor `requiresAuth.tsx` to simplify authentication flow by removi…
Ben-El Oct 26, 2025
467380e
Refactor `authContext.tsx` to streamline `AuthProvider`, remove `useE…
Ben-El Oct 26, 2025
c3a9aa1
Refactor `navbar.jsx` to simplify logout flow by removing conditional…
Ben-El Oct 26, 2025
708aa7e
Refactor `navbar.jsx` and `requiresAuth.tsx` to clean up unused impor…
Ben-El Oct 26, 2025
df0e3d3
Refactor `index.jsx` to adjust route nesting by relocating `index` ro…
Ben-El Oct 26, 2025
5cff9a9
Refactor `index.jsx` to relocate fallback route for improved route st…
Ben-El Oct 26, 2025
3784285
Refactor `layout.tsx` to remove unnecessary `useEffect` and streamlin…
Ben-El Oct 26, 2025
b64d6c9
Refactor `api.jsx` to improve unmount logic by adding `isMounted` che…
Ben-El Oct 26, 2025
9b354b6
Refactor `authContext.tsx`, `api.jsx`, and `user.jsx` to introduce `U…
Ben-El Oct 27, 2025
0ee5fdf
Refactor `authContext.tsx` to simplify `AuthProvider` by replacing `r…
Ben-El Oct 27, 2025
3727dbc
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Oct 27, 2025
f660e5b
Refactor `user.jsx` to handle `UNKNOWN` auth status, improve user-fet…
Ben-El Oct 27, 2025
af7cbc0
Refactor `navbar.jsx`, `login.tsx`, `api.jsx`, and `user.jsx` to impr…
Ben-El Oct 27, 2025
ba0c84e
Simplify logout logic in `navbar.jsx` by replacing hardcoded URL with…
Ben-El Oct 27, 2025
653134b
Remove `sessionStorage`-based logout flow logic from `navbar.jsx`, `l…
Ben-El Oct 27, 2025
24664d9
Remove unused `useEffect` import from `login.tsx`.
Ben-El Oct 27, 2025
505d523
Improve logout flow by finalizing `sessionStorage` logic in `navbar.j…
Ben-El Oct 27, 2025
4d859f9
Simplify logout logic in `navbar.jsx` by using `window.location.repla…
Ben-El Oct 27, 2025
b520906
Remove `sessionStorage`-based logout flow logic from `navbar.jsx`, `l…
Ben-El Oct 27, 2025
48f871b
Remove unused `auth` and `useAuth` imports from `navbar.jsx` and remo…
Ben-El Oct 27, 2025
3e18e3a
Cleanup `api.jsx` by simplifying `useEffect` cleanup logic and consis…
Ben-El Oct 27, 2025
107ce94
Add missing newline at end of `api.jsx` for consistency.
Ben-El Oct 27, 2025
d225ca5
Fix inconsistent object formatting in `api.jsx` within `setRequest` f…
Ben-El Oct 27, 2025
86237cf
Refactor `login.tsx` to streamline URL query handling and `ConfigProv…
Ben-El Oct 28, 2025
4f5679e
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Oct 28, 2025
2b700e5
Refactor `useUser` to simplify authentication status logic and update…
Ben-El Oct 28, 2025
1f03d70
Refactor `useUser` to enhance authentication status handling by impro…
Ben-El Oct 28, 2025
a407732
Refactor `useUser` to optimize `fetcher` logic and streamline authent…
Ben-El Oct 28, 2025
b6b4ee8
Update `RequiresAuth` to display loading state when user authenticati…
Ben-El Oct 28, 2025
a8f7a87
Refactor `RequiresAuth` and `useUser` to improve user authentication …
Ben-El Oct 28, 2025
047876d
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Oct 28, 2025
b954953
Refactor `useUser` to simplify `fetcher` logic by removing unnecessar…
Ben-El Oct 28, 2025
200ebfc
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Oct 28, 2025
96dbec9
Refactor `useUser` to handle unauthenticated status explicitly in `fe…
Ben-El Oct 28, 2025
bb29497
add changes
Ben-El Oct 29, 2025
addbe21
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Oct 29, 2025
ecfc0ec
Fix inconsistent formatting in `onClick` handler within `navbar.jsx`.
Ben-El Oct 29, 2025
6410f9b
Remove unused `logged` prop from `Layout` in `create-user-with-passwo…
Ben-El Oct 29, 2025
b5fc52c
Add `logged` prop to `Layout` in `create-user-with-password.jsx` to e…
Ben-El Oct 29, 2025
05e8336
changes
Ben-El Oct 29, 2025
1a6773e
Refactor login redirection logic to ensure `state` resets correctly a…
Ben-El Oct 29, 2025
10c074b
Remove unused `status` destructure in `useAPI` hook to clean up code.
Ben-El Oct 29, 2025
d6a3cca
Refactor login redirection logic and simplify unauthenticated error h…
Ben-El Oct 29, 2025
2a14c15
Fix typo in `Logout` label text within `navbar.jsx`.
Ben-El Oct 29, 2025
b32a916
Fix typo in `Logout` label and add missing return statement in `useAP…
Ben-El Oct 29, 2025
6a2193b
Refactor `RequiresAuth` to use `AUTH_STATUS` and centralize authentic…
Ben-El Oct 29, 2025
1f3b538
Simplify unauthenticated user check in `RequiresAuth` component.
Ben-El Oct 29, 2025
55d22a7
Refactor `useUser` hook to improve cache validation and enhance loadi…
Ben-El Oct 29, 2025
c96a60b
Add fallback to `getCurrentUser` in login flow to handle edge cases
Ben-El Oct 29, 2025
f36cdcf
Refactor `useUser` hook and login flow to streamline `getCurrentUser`…
Ben-El Oct 29, 2025
595a4ab
Enhance `useUser` hook with `pageshow` handling and optimize authenti…
Ben-El Oct 29, 2025
9ed12da
Refactor `useUser` hook to remove unused `status`, streamline `fetche…
Ben-El Oct 29, 2025
1a8074e
Remove redundant `getCurrentUser` call in login flow to streamline au…
Ben-El Oct 29, 2025
1593245
Refactor authentication flow to standardize `AUTH_STATUS` usage and i…
Ben-El Oct 29, 2025
a561a28
Simplify logout flow by removing redundant authentication state updates.
Ben-El Oct 29, 2025
a19849a
Remove unused `setAuthStatus` and clean up imports in `navbar.jsx`.
Ben-El Oct 29, 2025
a1188ba
Update logout flow to set `AUTH_STATUS.UNAUTHENTICATED` before redire…
Ben-El Oct 29, 2025
b021638
Simplify logout flow by removing `setAuthStatus`, cleaning up imports…
Ben-El Oct 29, 2025
7e90052
Set `AUTH_STATUS.UNAUTHENTICATED` in logout flow and clean up imports…
Ben-El Oct 29, 2025
e89c4c9
Add `setAuthStatus` to `navbar.jsx` and update imports for authentica…
Ben-El Oct 29, 2025
90f9f74
Simplify logout flow by replacing `onClick` logic with direct `href` …
Ben-El Oct 29, 2025
d5f1bf0
Update logout item to replace URL in history before redirect
Ben-El Oct 29, 2025
ef59744
Remove redundant comment in `RequiresAuth` component.
Ben-El Oct 29, 2025
2d586a7
Refactor authentication flow to check session cookies before making u…
Ben-El Oct 29, 2025
f69d2fe
Update logout flow to set `AUTH_STATUS.UNAUTHENTICATED` and use `wind…
Ben-El Oct 29, 2025
2052bec
Comment out unused `auth` import in `navbar.jsx`.
Ben-El Oct 29, 2025
5ebe478
Reset user state on logout by calling `auth.clearCurrentUser`.
Ben-El Oct 29, 2025
02bc69d
Refactor auth flow by simplifying cookie checks, consolidating logout…
Ben-El Oct 29, 2025
9c3e1a0
Update logout flow: clear user state and set `AUTH_STATUS.UNAUTHENTIC…
Ben-El Oct 29, 2025
221418a
Simplify logout item by replacing `onClick` logic with direct URL han…
Ben-El Oct 29, 2025
22ed250
Remove unused `auth` and `AUTH_STATUS` imports in `navbar.jsx`.
Ben-El Oct 29, 2025
7dd0fce
Refactor authentication flow: streamline session cookie checks, conso…
Ben-El Oct 29, 2025
bc11b5a
Remove unused `auth` and `AUTH_STATUS` imports from `navbar.jsx`.
Ben-El Oct 29, 2025
c0ccc4e
Remove unused `useEffect` and `pageshow` listener for session cookie …
Ben-El Oct 29, 2025
3fbc0d9
Remove unused `useEffect` import from `requiresAuth.tsx`.
Ben-El Oct 29, 2025
46b8b55
Update logout flow: clear user state, set `AUTH_STATUS.UNAUTHENTICATE…
Ben-El Oct 29, 2025
f51d9a5
Refactor authentication flow: unify session cookie checks, simplify s…
Ben-El Oct 29, 2025
973e1de
Remove unused `auth` import from `navbar.jsx`.
Ben-El Oct 29, 2025
1996fa2
Refactor logout flow: extract `handleLogout` function, update redirec…
Ben-El Oct 29, 2025
c006d22
Remove unused `happy-dom` router import and consolidate `LOGIN_COOKIE…
Ben-El Oct 29, 2025
5a0adaf
Refactor logout logic: replace router navigation with `window.history…
Ben-El Oct 29, 2025
ce93a8b
Refactor logout and authentication flow: simplify `handleLogout` logi…
Ben-El Oct 29, 2025
3fbe976
Remove unused `auth` import and redundant `as="button"` prop from `na…
Ben-El Oct 29, 2025
4617168
Add BFCache handling to `requiresAuth.tsx` and update session cookie …
Ben-El Oct 29, 2025
f8a68c6
Refactor authentication flow: simplify session handling, remove redun…
Ben-El Oct 29, 2025
f2482e3
Add BFCache and session cookie handling logic to `requiresAuth.tsx` t…
Ben-El Oct 29, 2025
1d7ef86
Add eslint-disable comment for unused `__lakefsBFGuard` declaration i…
Ben-El Oct 29, 2025
6d73424
Remove eslint-disable comment for unused `__lakefsBFGuard` declaratio…
Ben-El Oct 29, 2025
3a3fb5c
Update `onPageShow` type in `requiresAuth.tsx` for better type safety
Ben-El Oct 29, 2025
ee4b785
Remove unused BFCache and session cookie handling logic from `require…
Ben-El Oct 29, 2025
08bd31a
Refactor logout logic in `navbar.jsx`: add `auth.clearCurrentUser` ca…
Ben-El Oct 29, 2025
c0b6608
Enhance authentication redirection in `useAPI`: add 401 error handlin…
Ben-El Oct 29, 2025
24de77b
Refactor authentication redirection in `useAPI`: consolidate public a…
Ben-El Oct 29, 2025
0252da3
Refactor `useAPI` and `authContext`: consolidate unauthorized handlin…
Ben-El Oct 30, 2025
3bf75af
Remove unused `AUTH_STATUS` and `useNavigate` imports from `api.jsx` …
Ben-El Oct 30, 2025
f161ee7
Refactor `useUser` and `requiresAuth`: remove `useLocation` dependenc…
Ben-El Oct 30, 2025
483df71
Refactor `requiresAuth`: replace `useAuth` with `useUser` for simplif…
Ben-El Oct 30, 2025
3c52738
Comment out redundant `setRequest` and `return` logic in 401 error ha…
Ben-El Oct 30, 2025
82ec737
Remove commented-out redundant `setRequest` and `return` logic in `us…
Ben-El Oct 30, 2025
ea8e1db
Enhance login redirection: persist `next` state in sessionStorage and…
Ben-El Oct 30, 2025
2319d8a
Refactor login redirection: consolidate `next` state handling and str…
Ben-El Oct 30, 2025
aef4d21
Refactor login redirection: streamline `redirected` handling, normali…
Ben-El Oct 30, 2025
31855dd
Refactor login redirection: simplify `next` state handling and remove…
Ben-El Oct 30, 2025
c8ab095
Refactor login redirection: consolidate rendering logic, centralize a…
Ben-El Oct 30, 2025
f028b0b
Refactor login redirection: remove redundant `typeof` check for `next…
Ben-El Oct 30, 2025
c7de7fc
Refactor login redirection: extract `withNext` function, enhance `nex…
Ben-El Oct 30, 2025
521f459
Enhance login redirection: add `useEffect` to handle `post_login_next…
Ben-El Oct 30, 2025
c7c75e7
Refactor login flow: introduce `DEFAULT_LOGIN_CONFIG`, streamline log…
Ben-El Oct 30, 2025
3492a62
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Oct 30, 2025
9ebeeb0
Refactor login flow: streamline rendering logic, remove `DEFAULT_LOGI…
Ben-El Oct 30, 2025
b54602d
Refactor login redirection: simplify loading state, enhance `setup` r…
Ben-El Oct 30, 2025
4854532
Refactor login flow: extract `getLoginIntent`, introduce `DoNavigate`…
Ben-El Oct 31, 2025
6ddf7d5
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Nov 2, 2025
c0d46fc
Refactor navbar and login route: remove unused auth context reference…
Ben-El Nov 2, 2025
92dfe09
Refactor authentication handling: rename `setAuthStatus` to `setStatu…
Ben-El Nov 3, 2025
b3675eb
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Nov 3, 2025
2ce217a
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Nov 4, 2025
474adc6
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Nov 5, 2025
64cc7de
Refactor authentication flow: replace `buildNextFromWindow` with `get…
Ben-El Nov 6, 2025
4702fd5
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Nov 6, 2025
ab7efcb
Refactor authentication flow: remove `useUser` hook, centralize user …
Ben-El Nov 6, 2025
6bc2e24
commit
Ben-El Nov 7, 2025
dbb9341
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Nov 9, 2025
d0ecd0b
Enhance authContext: refresh user data on `pageshow` for better sessi…
Ben-El Nov 9, 2025
95ccf52
Refactor authentication handling: rename `onUnauthorized` to `onUnaut…
Ben-El Nov 9, 2025
c39f938
Refactor authContext and navbar: remove redundant dependencies, disab…
Ben-El Nov 9, 2025
917f6ae
Format imports in `authContext.tsx` for consistent styling.
Ben-El Nov 10, 2025
91eef37
commit
Ben-El Nov 10, 2025
717bf67
Merge branch 'master' into webui-refactor-centralize-authentication
Ben-El Nov 11, 2025
9c787c5
changes
Ben-El Nov 11, 2025
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
89 changes: 89 additions & 0 deletions webui/src/lib/auth/authContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, {createContext, useContext, useMemo, useState, ReactNode, useCallback, useEffect} from "react";
import { useNavigate } from "react-router-dom";
import { auth } from "../api";
import {getCurrentRelativeUrl, isPublicAuthRoute, ROUTES} from "../utils";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: what's the style, {aaa} or { aaa }?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ aaa }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import React, {createContext, useContext, useMemo, useState, ReactNode, useCallback, useEffect} from "react";
import { useNavigate } from "react-router-dom";
import { auth } from "../api";
import {getCurrentRelativeUrl, isPublicAuthRoute, ROUTES} from "../utils";
import React, { createContext, useContext, useMemo, useState, ReactNode, useCallback, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { auth } from "../api";
import { getCurrentRelativeUrl, isPublicAuthRoute, ROUTES } from "../utils";


export const LAKEFS_POST_LOGIN_NEXT = "lakefs_post_login_next";
export const AUTH_STATUS = {
AUTHENTICATED: "authenticated",
UNAUTHENTICATED: "unauthenticated",
PENDING: "pending",
} as const;

export type AuthStatus = typeof AUTH_STATUS[keyof typeof AUTH_STATUS];

type User = { id?: string } | null;

type AuthContextType = {
status: AuthStatus;
user: User;
refreshUser: (opts?: { useCache?: boolean }) => Promise<void>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment that explains this code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that refreshUser can optionally receive an object of options, currently only { useCache?: boolean }.
refreshUser is an async method to reload the current user state from the API.
It accepts an optional options object (currently { useCache?: boolean }),
allowing callers to control whether to use cached data or force a fresh fetch.

setStatus: (s: AuthStatus) => void;
onUnauthorized: () => void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why onUnauthorized and not onUnauthenticated?

};

const AuthContext = createContext<AuthContextType | null>(null);

export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [status, setStatus] = useState<AuthStatus>(AUTH_STATUS.PENDING);
const [user, setUser] = useState<User>(null);
const navigate = useNavigate();

const refreshUser = useCallback(
async ({ useCache = true }: { useCache?: boolean } = {}) => {
try {
const u = useCache
? await auth.getCurrentUserWithCache()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren’t you wrapping await auth.getCurrentUserWithCache() with useAPI?
Also, related to this, if you’re recalculating this while the user is in a loading state, shouldn’t you handle it by setting setStatus(AUTH_STATUS.PENDING)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided not to use useAPI here because the AuthContext runs outside the component lifecycle - we call refreshUser on app startup, after login/logout, and on pageshow events.
Using useAPI here would introduce redundant redirects and implicit 401 handling that conflict with onUnauthorized.
Regarding PENDING, I will add a selective state update: it will set only when performing a cold refresh (useCache: false) and the user isn't already authenticated.
This will avoid flicker during background updates while still providing a proper loading state.

: await auth.getCurrentUser();
const ok = Boolean(u?.id);
setUser(ok ? u : null);
setStatus(ok ? AUTH_STATUS.AUTHENTICATED : AUTH_STATUS.UNAUTHENTICATED);
} catch {
setUser(null);
setStatus(AUTH_STATUS.UNAUTHENTICATED);
}
},
[]
);

const onUnauthorized = useCallback(() => {
auth.clearCurrentUser();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This callback centralizes the "unauthorized" handling logic for the app.
It's used whenever the backend returns a 401 or when the user session becomes invalid.

Calling auth.clearCurrentUser() here ensures that any cached user info is cleared immediately, before redirecting to the login page.

Having this as a dedicated onUnauthorized function makes the flow reusable - we can call it both from API hooks and from other parts of the app that need to handle session expiration consistently.

setUser(null);
setStatus(AUTH_STATUS.UNAUTHENTICATED);

if (isPublicAuthRoute(window.location.pathname)) return;

navigate(ROUTES.LOGIN, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why redirect to ROUTES.LOGIN instead of to the root, which already redirects to the login page?
Also, shouldn’t the /logout endpoints handle this redirection already?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We redirect explicitly to ROUTES.LOGIN instead of / for a few reasons:

onUnauthorized is triggered by 401s from XHR/fetch calls anywhere in the app.
Redirects returned by the /logout endpoint don't affect SPA navigation (the browser will not change window.location), so the client must navigate explicitly.

Going directly to /auth/login avoids an extra hop (root → login) and reduces flicker.

It also guarantees we keep the intended next target and doesn't rely on whatever logic the root route currently implements.

We keep the isPublicAuthRoute guard to avoid redirect loops if we are already on the login/oidc/sso paths.

TL;DR: direct, deterministic navigation to the login page is safer and smoother here.

replace: true,
state: { redirected: true, next: getCurrentRelativeUrl() },
});
}, [navigate]);

useEffect(() => { void refreshUser({ useCache: true }); }, [refreshUser]);

useEffect(() => {
if (status === AUTH_STATUS.AUTHENTICATED) {
const postLoginNext = window.sessionStorage.getItem(LAKEFS_POST_LOGIN_NEXT);
if (postLoginNext && postLoginNext.startsWith("/")) {
window.sessionStorage.removeItem(LAKEFS_POST_LOGIN_NEXT);
const next = getCurrentRelativeUrl();
if (next !== postLoginNext) {
navigate(postLoginNext, { replace: true });
}
}
}
}, [status, navigate]);

const value = useMemo<AuthContextType>(
() => ({ status, user, refreshUser, setStatus, onUnauthorized }),
[status, user, refreshUser, onUnauthorized]
);

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

export const useAuth = (): AuthContextType => {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within <AuthProvider>");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this happen?
Isn't AuthContext initialized no matter what?

Copy link
Contributor Author

@Ben-El Ben-El Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can happen if a component using useAuth() is rendered outside the <AuthProvider>, for example in a storybook, or future refactors where a subtree is mounted separately.

The guard makes the failure explicit and easy to debug, instead of causing null errors later.
So it's a safety net, it should never trigger in production, but it's still valuable during development.

return ctx;
};
20 changes: 1 addition & 19 deletions webui/src/lib/components/auth/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useEffect, useState} from "react";
import React, {useState} from "react";
import {Outlet, useOutletContext} from "react-router-dom";
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
Expand All @@ -8,8 +8,6 @@ import Card from "react-bootstrap/Card";

import {Link} from "../nav";
import {useLoginConfigContext} from "../../hooks/conf";
import {useLayoutOutletContext} from "../layout";
import {useRouter} from "../../hooks/router";
import Alert from "react-bootstrap/Alert";
import {InfoIcon} from "@primer/octicons-react";

Expand All @@ -20,22 +18,6 @@ export const AuthLayout = () => {
const [showRBACAlert, setShowRBACAlert] = useState(!window.localStorage.getItem(rbacDismissedKey));
const [activeTab, setActiveTab] = useState("credentials");
const {RBAC: rbac} = useLoginConfigContext();
const [isLogged] = useLayoutOutletContext();
const router = useRouter();

useEffect(() => {
if (!isLogged) {
// Redirect to the login page here, instead of in Layout where isLogged is set, because Layout also wraps
// routes that don't require authentication, and redirecting would be incorrect.
router.push({
pathname: '/auth/login',
params: {},
query: { next: '/', redirected: 'true' },
});
}
}, [isLogged, router]);

if (!isLogged) return null;

return (
<Container fluid="xl">
Expand Down
41 changes: 15 additions & 26 deletions webui/src/lib/components/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,30 @@
import React, { FC, useContext, useState, useEffect } from "react";
import { Outlet, useOutletContext } from "react-router-dom";
import React, { FC, useContext, useEffect } from "react";
import { Outlet } from "react-router-dom";
import { ConfigProvider } from "../hooks/configProvider";
import TopNav from './navbar';
import { AppContext } from "../hooks/appContext";
import useUser from "../hooks/user";
import {AUTH_STATUS, useAuth} from "../auth/authContext";

type LayoutOutletContext = [boolean];
const Layout: FC = () => {
const { status } = useAuth();
const showTopNav = status === AUTH_STATUS.AUTHENTICATED;

const Layout: FC<{logged: boolean}> = ({logged}) => {
const [isLogged, setIsLogged] = useState(logged ?? true);
const { user, loading, error } = useUser();
const userWithId = user as { id?: string } | null;

// Update isLogged state based on actual authentication status
const { state } = useContext(AppContext);
useEffect(() => {
if (!loading) {
// If there's a user and no error, show authenticated (full) navbar
setIsLogged(!!userWithId?.id && !error);
}
}, [userWithId, loading, error]);

// handle global dark mode here
const {state} = useContext(AppContext);
document.documentElement.setAttribute('data-bs-theme', state.settings.darkMode ? 'dark' : 'light')
document.documentElement.setAttribute(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this is inside useEffect?
Did you test the behavior of the Dark Mode after this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question!
I moved this logic into useEffect because setting a DOM attribute is a side-effect,
and React best practices recommend running side-effects inside useEffect rather than during render.
This ensures that the attribute is updated only when darkMode changes, and prevents double execution under strict mode.
I tested it and confirmed that dark mode toggling and initial theme load still behave as expected.

"data-bs-theme",
state.settings.darkMode ? "dark" : "light"
);
}, [state.settings.darkMode]);

return (
<ConfigProvider>
{!loading && <TopNav logged={isLogged}/>}
{showTopNav && <TopNav/>}
<div className="main-app">
<Outlet context={[isLogged] satisfies LayoutOutletContext}/>
<Outlet />
</div>
</ConfigProvider>
);
};

export function useLayoutOutletContext() {
return useOutletContext<LayoutOutletContext>();
}

export default Layout;
export default Layout;
17 changes: 9 additions & 8 deletions webui/src/lib/components/navbar.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import React from "react";
import useUser from '../hooks/user'
import {auth} from "../api";
import {useRouter} from "../hooks/router";
import {Link} from "./nav";
import DarkModeToggle from "./darkModeToggle";
Expand All @@ -9,15 +7,18 @@ import Container from "react-bootstrap/Container";
import {useLoginConfigContext} from "../hooks/conf";
import {FeedPersonIcon} from "@primer/octicons-react";
import {useConfigContext} from "../hooks/configProvider";
import {auth} from "../api";
import {AUTH_STATUS, useAuth} from "../auth/authContext";

const NavUserInfo = () => {
const { user, loading: userLoading, error } = useUser();
const { user, status } = useAuth();
const userLoading = status === AUTH_STATUS.PENDING;
const logoutUrl = useLoginConfigContext()?.logout_url || "/logout"
const {config, error: versionError, loading: versionLoading} = useConfigContext();
const versionConfig = config?.versionConfig || {};

if (userLoading || versionLoading) return <Navbar.Text>Loading...</Navbar.Text>;
if (!user || !!error) return (<></>);
if (!user) return (<></>);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think that this is an example why await auth.getCurrentUserWithCache() should be wrapped in useAPI ,so errors can be returned and handled like before:

if (!user || !!error) return (<></>);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our current setup, all authentication errors are handled centrally in AuthContext via onUnauthorized - that logic already sets UNAUTHENTICATED and navigates to /auth/login when needed.

The navbar doesn't need to handle auth errors itself - if user is null, we simply don't render the user section.
Using useAPI here would duplicate the logic and risk inconsistent auth state between AuthContext and UI components.

TL;DR: Auth errors are now global, not per-component - that's why this simplified check (if (!user) return <></>) is sufficient.

const notifyNewVersion = !versionLoading && !versionError && versionConfig.upgrade_recommended
const NavBarTitle = () => {
return (
Expand All @@ -37,9 +38,9 @@ const NavUserInfo = () => {
</>
</NavDropdown.Item><NavDropdown.Divider/></>}
<NavDropdown.Item
onClick={()=> {
onClick={() => {
auth.clearCurrentUser();
window.location = logoutUrl;
window.location.replace(logoutUrl);
}}>
Logout
</NavDropdown.Item>
Expand All @@ -63,8 +64,8 @@ const TopNavLink = ({ href, children }) => {
);
};

const TopNav = ({logged = true}) => {
return logged && (
const TopNav = () => {
return (
<Navbar variant="dark" bg="dark" expand="md" className="border-bottom">
<Container fluid={true}>
<Link component={Navbar.Brand} href="/">
Expand Down
21 changes: 21 additions & 0 deletions webui/src/lib/components/requiresAuth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from "react";
import {Navigate, Outlet} from "react-router-dom";
import {Loading} from "./controls";
import {ROUTES} from "../utils";
import {getCurrentRelativeUrl} from "../utils";
import {AUTH_STATUS, useAuth} from "../auth/authContext";

const RequiresAuth: React.FC = () => {
const { user, status } = useAuth();

if (status === AUTH_STATUS.PENDING) return <Loading />;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think thay when the user is recalculated, AUTH_STATUS.PENDING should be set in the meantime in authContext since ; should appear while loading

if (!user) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also handle !user.id , the user might exist, but a null id also means no authenticated user.

Copy link
Contributor Author

@Ben-El Ben-El Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A missing user.id indeed means the user isn't authenticated.
In the current implementation of AuthContext, we already normalize that: setUser(ok ? u : null) ensures user is null whenever id is missing.
So if (!user) already covers both cases.

const next = getCurrentRelativeUrl();
const params = new URLSearchParams({ redirected: "true", next });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why URLSearchParams gets { redirected: "true", next } as input and not location.search ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm explicitly creating a new URLSearchParams({ redirected: "true", next }) rather than using location.search because here we want to build a clean and controlled query string for the redirect to /login.

Using location.search would carry over any existing query params from the previous route - which might be irrelevant or even cause bugs during login.

This way we ensure that the login page only receives the parameters it actually expects:
redirected=true and next=<path>.

return <Navigate to={{ pathname: ROUTES.LOGIN, search: `?${params.toString()}` }} replace state={{ redirected: true, next }}/>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use location.search instead of params.toString()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used a new URLSearchParams here rather than location.search because we want to build a clean redirect query containing only redirected=true and next=<path>.
Using location.search would risk carrying over unrelated query params from the previous page.

URLSearchParams also ensures proper encoding for next values like /repositories, so this approach is safer and more predictable for redirects.

}

return <Outlet/>;
};

export default RequiresAuth;
39 changes: 7 additions & 32 deletions webui/src/lib/hooks/api.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useEffect, useState} from 'react';
import {AuthenticationError} from "../api";
import {useRouter} from "./router";
import {useAuth} from "../auth/authContext";

const initialPaginationState = {
loading: true,
Expand Down Expand Up @@ -50,46 +50,21 @@ const initialAPIState = {
};

export const useAPI = (promise, deps = []) => {
const router = useRouter();
const [request, setRequest] = useState(initialAPIState);
const [needToLogin, setNeedToLogin] = useState(false);

useEffect(() => {
if (needToLogin) {
const loginPathname = '/auth/login';
if (router.route === loginPathname) {
return;
}
// If the user is not logged in and attempts to access a lakeFS endpoint other than '/auth/login',
// they are first redirected to the '/auth/login' endpoint. For users logging in via lakeFS
// (not via SSO), after successful authentication they will be redirected back to the original endpoint
// they attempted to access. The redirected flag is set here so it can later be used to properly
// handle SSO redirection when login via SSO is configured.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep a comment (where it should be in the new version) explaining why the redirected flag is set to true, since it wasn’t clear to either Barak or me when we first saw it.

router.push({
pathname: loginPathname,
query: {next: router.route, redirected: true},
});
setNeedToLogin(false);
}
}, [needToLogin, router])
const { onUnauthorized } = useAuth();

useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f we’re changing this anyway, maybe it’s worth also adding the if (isMounted) check before the

setRequest({
  loading: false,
  error: null,
  response,
});

call to guard against updating state after the component unmounts? - WDYT

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

let isMounted = true;
setRequest(initialAPIState);
const execute = async () => {
try {
const response = await promise();
setRequest({
loading: false,
error: null,
response,
});
if (!isMounted) return;
setRequest({ loading: false, error: null, response });
} catch (error) {
if (error instanceof AuthenticationError) {
if (isMounted) {
setNeedToLogin(true);
}
return;
if (!isMounted) return;
if (error instanceof AuthenticationError && error.status === 401) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AuthenticationError is the status code 401 isn't it? why did you add && error.status === 401?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AuthenticationError is generally used for 401 responses.
However, there could be cases where AuthenticationError was thrown for other auth-related situations (for example, expired tokens, misconfigured cookies, or API edge-cases) that didn't have a strict 401 status.

Adding the explicit error.status === 401 check makes the intent clearer - it ensures we only trigger the “unauthorized” flow when the backend explicitly signals a 401, and not for other possible AuthenticationError subclasses or wrappers.

In short: it's a defensive check that prevents false positives and makes the logic more robust.

onUnauthorized();
}
setRequest({
loading: false,
Expand Down
29 changes: 16 additions & 13 deletions webui/src/lib/hooks/configProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { createContext, FC, useContext, useEffect, useState, } from "react";
import React, {createContext, FC, useContext, useEffect, useMemo} from "react";

import { config } from "../api";
import useUser from "./user";
import { usePluginManager } from "../../extendable/plugins/pluginsContext";
import {useAPI} from "./api";
import {useAuth} from "../auth/authContext";

type ConfigContextType = {
error: Error | null;
Expand Down Expand Up @@ -57,21 +58,23 @@ const useConfigContext = () => useContext(configContext);

const ConfigProvider: FC<{children: React.ReactNode}> = ({children}) => {
const pluginManager = usePluginManager();
const {user} = useUser();
const [storageConfig, setConfig] = useState<ConfigContextType>(configInitialState);
const {user} = useAuth();
const { response, loading, error } = useAPI(() => config.getConfig(), [user]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥


useEffect(() => {
config.getConfig()
.then(configData => {
pluginManager.customObjectRenderers?.init(configData);
setConfig({config: configData, loading: false, error: null});
})
.catch((error) =>
setConfig({config: null, loading: false, error}));
}, [user]);
if (response) {
pluginManager.customObjectRenderers?.init(response);
}
}, [response, pluginManager]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the useEffect needed here?

Copy link
Contributor Author

@Ben-El Ben-El Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

init is an imperative side-effect, so we shouldn't fire it during render.
Running it in useEffect ensures it executes after commit and only when response (or the pluginManager instance) changes.
This also avoids double runs in React StrictMode.
(If pluginManager is stable by identity, we could drop it from the deps, but keeping it is safer).


const value = useMemo(
() => (
{ config: response ?? null, loading, error } satisfies ConfigContextType
),
[response, loading, error]);

return (
<configContext.Provider value={storageConfig}>
<configContext.Provider value={value}>
{children}
</configContext.Provider>
);
Expand Down
10 changes: 0 additions & 10 deletions webui/src/lib/hooks/user.jsx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that from a design perspective, the useAuth logic should depend on the response from auth.getCurrentUserWithCache().
i.e, the authentication state should be determined based on auth.getCurrentUserWithCache(), rather than the other way around as I see here i.e., calling auth.getCurrentUser() only when status === AUTH_STATUS.UNAUTHENTICATED.

The purpose of the /user endpoint is to return the current user data. If user.id is null, it means the user is unauthenticated, else it is authenticated.
So, instead of basing the auth state on frontend manipulations of useAuth's status by setting the setAuthStatus, we should rely on the backend /user response.

To determine the status inside useAuth(), we should call the original code of useUser(.

@itaigilo - WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @Annaseli here.
This hook is already minimal -
It can be unified with the authContext for simplicity and easier tracking.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ben-El this comment still awaits your addressing.

This file was deleted.

Loading
Loading