bugfix: organization not selected problem fixed. being logged out too often fixed.

This commit is contained in:
2026-07-12 17:51:31 +03:30
parent 39935688ad
commit 901d838a2c
14 changed files with 490 additions and 22 deletions

View File

@@ -11,6 +11,11 @@ import {
} from '@/lib/auth/rememberMe';
import { User, Organization } from '@/types/organization';
import { isAppLocale, getLocaleFromPathname } from '@/i18n/routing';
import {
clearAccessTokenExpiresAt,
rememberAccessTokenExpiresAt,
startProactiveSessionRefresh,
} from '@/lib/auth/proactiveRefresh';
interface AuthContextType {
user: User | null;
@@ -91,6 +96,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
if (response.success) {
const { user: userData, organizations: orgs } = normalizeProfilePayload(response.data);
rememberAccessTokenExpiresAt(
(response.data as { accessTokenExpiresAt?: string }).accessTokenExpiresAt,
);
setUser(userData);
setOrganizations(orgs);
@@ -99,17 +107,29 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
if (storedOrgId && orgs.length > 0) {
const org = orgs.find(o => o.id === storedOrgId);
if (org) {
setCurrentOrganization(org);
// Ensure cookie token carries organizationId for org-scoped APIs.
await authApi.selectOrganization(org.id);
const selected = await authApi.selectOrganization(org.id);
setCurrentOrganization({
id: selected.data.organization.id,
name: selected.data.organization.name,
type: selected.data.organization.type as Organization['type'],
isOwner: Boolean(selected.data.organization.isOwner),
permissions: selected.data.organization.permissions,
plan: selected.data.organization.plan,
});
} else {
setCurrentOrganization(null);
}
} else if (orgs.length === 1 && userData) {
setCurrentOrganization(orgs[0]);
const selected = await authApi.selectOrganization(orgs[0].id);
localStorage.setItem('currentOrganizationId', orgs[0].id);
// Keep JWT in sync with selected org even for single-org users.
await authApi.selectOrganization(orgs[0].id);
setCurrentOrganization({
id: selected.data.organization.id,
name: selected.data.organization.name,
type: selected.data.organization.type as Organization['type'],
isOwner: Boolean(selected.data.organization.isOwner),
permissions: selected.data.organization.permissions,
plan: selected.data.organization.plan,
});
} else {
setCurrentOrganization(null);
}
@@ -119,6 +139,61 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
(err as { statusCode?: number })?.statusCode ??
(err as { response?: { status?: number } })?.response?.status;
// Access token may have expired while refresh cookie is still valid (e.g. JWT_EXPIRES_IN=15m).
if (status === 401) {
try {
const refreshResult = await authApi.refreshSessionFromCookies();
rememberAccessTokenExpiresAt(refreshResult.data?.accessTokenExpiresAt);
const orgId = localStorage.getItem('currentOrganizationId');
if (orgId) {
await authApi.selectOrganization(orgId);
}
const retry = await authApi.getProfile();
if (retry.success) {
const { user: userData, organizations: orgs } = normalizeProfilePayload(retry.data);
rememberAccessTokenExpiresAt(
(retry.data as { accessTokenExpiresAt?: string }).accessTokenExpiresAt,
);
setUser(userData);
setOrganizations(orgs);
const storedOrgId = localStorage.getItem('currentOrganizationId');
if (storedOrgId && orgs.length > 0) {
const org = orgs.find((o) => o.id === storedOrgId);
if (org) {
const selected = await authApi.selectOrganization(org.id);
setCurrentOrganization({
id: selected.data.organization.id,
name: selected.data.organization.name,
type: selected.data.organization.type as Organization['type'],
isOwner: Boolean(selected.data.organization.isOwner),
permissions: selected.data.organization.permissions,
plan: selected.data.organization.plan,
});
} else {
setCurrentOrganization(null);
}
} else if (orgs.length === 1 && userData) {
const selected = await authApi.selectOrganization(orgs[0].id);
localStorage.setItem('currentOrganizationId', orgs[0].id);
setCurrentOrganization({
id: selected.data.organization.id,
name: selected.data.organization.name,
type: selected.data.organization.type as Organization['type'],
isOwner: Boolean(selected.data.organization.isOwner),
permissions: selected.data.organization.permissions,
plan: selected.data.organization.plan,
});
} else {
setCurrentOrganization(null);
}
return;
}
} catch {
/* fall through to logged-out state */
}
}
// 401 on profile is expected when there is no session — not an application error.
if (status !== 401) {
console.error('Auth check failed:', err);
@@ -128,6 +203,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setUser(null);
setOrganizations([]);
setCurrentOrganization(null);
clearAccessTokenExpiresAt();
} finally {
setIsLoading(false);
setIsAuthReady(true);
@@ -138,6 +214,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
void checkAuth();
}, [checkAuth]);
useEffect(() => {
if (!user || !isAuthReady) {
return;
}
return startProactiveSessionRefresh();
}, [user, isAuthReady]);
const applyUrlLocaleToUser = useCallback(async (user: User): Promise<User> => {
if (typeof window === 'undefined') return user;
@@ -180,6 +264,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
});
const userData = await applyUrlLocaleToUser(response.data.user);
rememberAccessTokenExpiresAt(response.data.accessTokenExpiresAt);
setUser(userData);
setOrganizations(response.data.organizations);
@@ -223,6 +308,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}
const userData = await applyUrlLocaleToUser(response.data.user);
rememberAccessTokenExpiresAt(response.data.accessTokenExpiresAt);
setUser(userData);
setOrganizations(response.data.organizations);
@@ -263,6 +349,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setOrganizations([]);
setCurrentOrganization(null);
setError(null);
clearAccessTokenExpiresAt();
setIsAuthReady(true);
router.replace('/');
router.refresh();