Skip to content

Conversation

Annaseli
Copy link
Contributor

Closes #9505

Description

This PR fixes all the issues mentioned in the related issue.

I tested the login flow and confirmed that login flow works as expected, with all previously reported problems resolved in the following environments:

  1. Local lakeFS OSS with no RBAC
  2. Local lakeFS OSS with ACL
  3. Local lakeFS Enterprise with local RBAC

@Annaseli Annaseli added the include-changelog PR description should be included in next release changelog label Sep 11, 2025
hasMore = results.pagination.has_more;
} while (hasMore);
usersList.sort((a, b) => resolveUserDisplayNameFN(a).localeCompare(resolveUserDisplayNameFN(b)));
return usersList;
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 catch here prevented the error from being caught by useAPI, which is responsible for redirecting to /auth/login. Since this catch block didn’t do anything with the error, I removed it.

}
setIsLogged(true);
}
}, [userWithId, loading, error, setIsLogged, router]);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

By using useEffect, we can redirect to the login page from all of the Administration tabs instead of showing errors or an empty pages.

Copy link
Contributor

Choose a reason for hiding this comment

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

You're already handling this in layout.tsx -
I think it's way better to do this routing there.
It keeps everything in one place, and doesn't require importing the useLayoutOutletContext.
It might catch future redirections that might occur in the "app layout" level.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As discussed, I’m keeping this router.push() code here and will open a relevant issue for refactoring.

// If there's no user or there's an authentication error, show simple navbar
setIsLogged(Boolean((userWithId && userWithId.id !== "") && !error));
}
}, [userWithId, loading, error]);
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 fix ensures the navbar is displayed according to the user’s current authentication status.

query: {next: router.route, redirected: true},
});
return null;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Endpoints starting with /repositories return AlertError here before reaching the useAPI func that should redirect to /auth/login when the user is unauthenticated. i.e, the error occurs before the useAPI code executes, so before this change the error was simply displayed instead of triggering the redirect.

Copy link
Contributor

@itaigilo itaigilo left a comment

Choose a reason for hiding this comment

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

Nice catch - good handling of some nasty bugs.

Blocking for the RepoError comments -
The code should be simplified and cleaned.

params: {},
query: { next: router.route, redirected: 'true' },
});
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is very confusing.
Prefer else instead of return in this case.

setIsLogged(true);
}, [setIsLogged]);
if (!loading) {
if (!userWithId || userWithId.id === "" || error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can there be a userWithId which is not null but without id?

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 get userWithId from useUser(), which calls auth.getCurrentUserWithCache(). If the user data isn’t cached, this function fetches it from the /user endpoint.

According to Swagger, the /user endpoint when returns status code 200, returns a CurrentUser schema, which returns the User schema:

    User:
      type: object
      required:
        - id
        - creation_date
      properties:
        id:
          type: string
          description: A unique identifier for the user. Cannot be edited.
        creation_date:
          type: integer
          format: int64
          description: Unix Epoch in seconds
...

Because id is a required field in this schema, the id property must exist when userWithId is not null.

Also in the browser, when the user is unauthenticated, we see:

image

Copy link
Contributor

Choose a reason for hiding this comment

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

So if (!userWithId || error) { should be enough, no?
Since id can't be empty?

(It's nit, ofc.)

Copy link
Contributor Author

@Annaseli Annaseli Sep 21, 2025

Choose a reason for hiding this comment

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

@itaigilo
userWithId.id is always present, but it can be an empty string. It’s empty when the user is logged out. So, checking whether this value is empty is the key test for determining if the user is logged in or not.

if (!loading) {
// If there's a user and no error, show authenticated (full) navbar
// If there's no user or there's an authentication error, show simple navbar
setIsLogged(Boolean((userWithId && userWithId.id !== "") && !error));
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
setIsLogged(Boolean((userWithId && userWithId.id !== "") && !error));
setIsLogged(userWithId?.id !== "" && !error);

Copy link
Contributor

Choose a reason for hiding this comment

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

But double-check this one ☝️ 😄

Copy link
Contributor Author

@Annaseli Annaseli Sep 16, 2025

Choose a reason for hiding this comment

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

For the:
setIsLogged(userWithId?.id !== "" && !error);

there will be an issue when userWithId is null or undefined:
null?.id !== "" → undefined !== "" → true so if there is no error we will get: true && !error -> true

while with the current code:
null && * → false

So the behavior differs.

But I did remove the redundant parentheses around (userWithId && userWithId.id !== "") and replaced the Boolean() cast with !! (since omitting it leads to a TypeScript error):

image

};

export const RepoError = ({error}) => {
export const RepoError = ({error, router}) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

There's no need to pass router as param -
You can have useRouter() here.

export const RepoError = ({error}) => {
export const RepoError = ({error, router}) => {
if (error instanceof AuthenticationError) {
router.push({
Copy link
Contributor

Choose a reason for hiding this comment

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

I would expect this RepoError component to contain only a rendering of the UI, not routing logic.
We should strive to have the routing logic at the "top" of the components tree, otherwise it becomes a nightmare to understand, for example, which component ended up causing the router.push(), making it very hard to maintain and to debug.

Have you tried placing it in the place where this error is originally produced?
I think that RefContextProvider should be it, but I'm not sure it's the source for all these RepoErrors.

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 changed the code so that the RefContextProvider now uses useAPI() since useAPI() automatically redirects to the login page when the user is unauthenticated. Changed the RefContextProvider because useRefs() receives its data from the RefContextProvider, and the errors returned from useRefs() are passed to the RepoError component for display.

Copy link
Contributor

@nopcoder nopcoder left a comment

Choose a reason for hiding this comment

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

Don't think that RepoError should redirect - at this level when we render a control to display an error we should not make this call.

The useAPI and similar that handle the API call should handle this state and update that we need to redirect user to the login page.

@Annaseli
Copy link
Contributor Author

Don't think that RepoError should redirect - at this level when we render a control to display an error we should not make this call.

The useAPI and similar that handle the API call should handle this state and update that we need to redirect user to the login page.

I changed the code so that the RefContextProvider now uses useAPI() since useAPI() automatically redirects to the login page when the user is unauthenticated. Changed the RefContextProvider because useRefs() receives its data from the RefContextProvider, and the errors returned from useRefs() are passed to the RepoError component for display.

Copy link
Contributor

@itaigilo itaigilo left a comment

Choose a reason for hiding this comment

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

Looking much better now.

Still some comments about the redirection.

setIsLogged(true);
}, [setIsLogged]);
if (!loading) {
if (!userWithId || userWithId.id === "" || error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

So if (!userWithId || error) { should be enough, no?
Since id can't be empty?

(It's nit, ofc.)

}

// Don't render anything if not authenticated (will redirect)
if (!userWithId || userWithId.id === "" || error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I suggest loading in this case as well (better see a loader than a blank screen that might look like something is stuck) -
But up to you.

Copy link
Contributor Author

@Annaseli Annaseli Sep 21, 2025

Choose a reason for hiding this comment

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

consulted about it also with chatGPT and it says:

short answer: show a loading state only when the auth status is still unknown; otherwise keep return null while you redirect.

Why

If you’re already sure isLogged === false, you immediately redirect in the effect. Rendering a spinner for a frame just before redirect often causes a flicker. return null is fine here.

Showing a spinner makes sense when auth is still loading (e.g., useUser() hasn’t resolved yet).


so I think i'll leave the return null and return the only while loading.

return (
<ConfigProvider>
<TopNav logged={isLogged}/>
{!loading && <TopNav logged={isLogged}/>}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is loading == true only after the login form is submitted?
Or are there other situations?

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 loading comes from the useUser() call. It is loading == true only while waiting for the /user data to be fetched.

const { response, error, loading } = useAPI(async () => {
if (!repoId) return null;
const repo = await repositories.get(repoId);
const reference = await resolveRef(repoId, (ref) ? ref : repo.default_branch);
Copy link
Contributor

Choose a reason for hiding this comment

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

While you're at it:

const { response, error, loading } = useAPI(async () => {
if (!repoId) return null;
const repo = await repositories.get(repoId);
const reference = await resolveRef(repoId, (ref) ? ref : repo.default_branch);
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
const reference = await resolveRef(repoId, (ref) ? ref : repo.default_branch);
const reference = await resolveRef(repoId, ref || repo.default_branch);

if (!repoId) return null;
const repo = await repositories.get(repoId);
const reference = await resolveRef(repoId, (ref) ? ref : repo.default_branch);
const comparedRef = await resolveRef(repoId, (compare)? compare : repo.default_branch);
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
const comparedRef = await resolveRef(repoId, (compare)? compare : repo.default_branch);
const comparedRef = await resolveRef(repoId, compare || repo.default_branch);

}
setIsLogged(true);
}
}, [userWithId, loading, error, setIsLogged, router]);
Copy link
Contributor

Choose a reason for hiding this comment

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

You're already handling this in layout.tsx -
I think it's way better to do this routing there.
It keeps everything in one place, and doesn't require importing the useLayoutOutletContext.
It might catch future redirections that might occur in the "app layout" level.

@Annaseli Annaseli requested a review from itaigilo September 21, 2025 14:10
const [activeTab, setActiveTab] = useState("credentials");
const {RBAC: rbac} = useLoginConfigContext();
const [setIsLogged] = useLayoutOutletContext();
const [loading, isLogged] = useLayoutOutletContext();
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be:
const [isLogged, setIsLogged] = useLayoutOutletContext();
?

Why it has a different structure?

Copy link
Contributor Author

@Annaseli Annaseli Sep 22, 2025

Choose a reason for hiding this comment

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

@itaigilo
I updated the parent Layout component to:
<Outlet context={[loading, isLogged, setIsLogged] satisfies LayoutOutletContext}/>

instead of:
<Outlet context={[setIsLogged] satisfies LayoutOutletContext}/>

I need const [loading, isLogged] = useLayoutOutletContext(); and not const [isLogged, setIsLogged] = useLayoutOutletContext(); because in AuthLayout I check whether the user is logged in. Since this check is already done in Layout, I can use the isLogged value passed through the context, rather than re-calling useUser(). So I needed the isLogged and didn't need the setIsLogged . There is no usage in setIsLogged in the updated AuthLayout component. (It also doesn't pass it to other components)

And I added loading to the useLayoutOutletContext () because it is returned from calling to the useUser (which is called in Layout). I need the loading in the AuthLayout so that I can add this:
if (loading) return <Loading/>

So AuthLayout needs both isLogged and loading via useLayoutOutletContext, and not the setIsLogged

@Annaseli Annaseli requested a review from itaigilo September 22, 2025 12:23
Copy link
Contributor

Choose a reason for hiding this comment

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

Pretty weird, to have only whitespace changes for files.
Next time, please try to avoid this.

useEffect(() => {
if (!loading) {
// If there's a user and no error, show authenticated (full) navbar
setIsLogged(!!(userWithId && userWithId.id !== "" && !error));
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
setIsLogged(!!(userWithId && userWithId.id !== "" && !error));
setIsLogged(!(userWithId?.id) && !error);

Copy link
Contributor Author

@Annaseli Annaseli Sep 22, 2025

Choose a reason for hiding this comment

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

I think that !(userWithId?.id) && !error is the opposite of !!(userWithId && userWithId.id !== "" && !error) because:

for !(userWithId?.id) && !error:
if user is logged out and there is no error => userWithId.id=="" => userWithId?.id is false => !(userWithId?.id) is true => !(userWithId?.id) && !error is true
so in this case when user is logged out we get setIsLogged(true) instead of setIsLogged(false)

Did you mean this?
setIsLogged(!!userWithId?.id && !error); (!! equals Boolean())

I updated the code to this - setIsLogged(!!userWithId?.id && !error);
but LMK if you meant something else.

setIsLogged(true);
}, [setIsLogged]);
if (!isLogged) {
router.push({
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 please add a comment here, explaining why this redirection is done here, and not it the place that gets the isLogged in first place (the top-level layout)?

return { repo, reference, compare: comparedRef };
}, [repoId, ref, compare]);

const refState = {
Copy link
Contributor

Choose a reason for hiding this comment

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

So refState is a const, and nothing changes it?
Or am I missing something?
What makes it a state?

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 "state" behavior comes from the useAPI hook.
When the async operation in useAPI completes or dependencies [repoId, ref, compare] change, useAPI triggers a re-render.
On each re-render, a new refState object is created with the updated values from useAPI.
This new object gets passed through the context, notifying all consumers.
So while refState is const within each render, it's recreated on every render with potentially different values.
So It's a derived state - refState is derived from the useAPI hook's internal state, not a state variable itself.

And useAPI already uses useEffect to execute the promise (fetch the data in this case).
It initializes state and manages loading and error handling internally, so we delegate that responsibility to useAPI.

Copy link
Contributor

Choose a reason for hiding this comment

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

Great, thanks for the details.

To make things (maybe) clearer, I would've renamed refState to refsState (which is nit),
And change the const [ refs ] = useContext(RefContext); above to const [ refsState ] = useContext(RefContext);. This way, it might be clearer that this const is the only object that actually accesses the refsState you create here.

But that's not blocking.

@Annaseli Annaseli requested a review from itaigilo September 22, 2025 22:49
Copy link
Contributor

@itaigilo itaigilo left a comment

Choose a reason for hiding this comment

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

LGTM -
Nice effort in a messy part of the code 💪

return { repo, reference, compare: comparedRef };
}, [repoId, ref, compare]);

const refState = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Great, thanks for the details.

To make things (maybe) clearer, I would've renamed refState to refsState (which is nit),
And change the const [ refs ] = useContext(RefContext); above to const [ refsState ] = useContext(RefContext);. This way, it might be clearer that this const is the only object that actually accesses the refsState you create here.

But that's not blocking.

@Annaseli Annaseli dismissed nopcoder’s stale review September 23, 2025 08:46

Addressed Barak Amar’s comment and he didn’t request any further changes. After Barak’s review, Itai Gilo also reviewed the code and approved it.

@Annaseli Annaseli merged commit 36d3e1b into master Sep 23, 2025
41 checks passed
@Annaseli Annaseli deleted the task/fix-missing-redirection-to-login branch September 23, 2025 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

include-changelog PR description should be included in next release changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Login flow issues when user is logged out

3 participants