Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use client';
import { useState } from 'react';
import { useSelector } from 'react-redux';
import {
selectAuthUserState,
useRefreshAuthMutation,
AccessTokenInfoResponse,
AuthUserSchema,
} from '@/entities/Auth';
import { LS_KEYS } from '@/shared/const/LS_KEYS';

const AuthRefreshTest = () => {
const [refreshAuth, { isLoading, error }] = useRefreshAuthMutation();
const authUser = useSelector((state: { authUser: AuthUserSchema }) =>
selectAuthUserState(state),
);
const [response, setResponse] = useState<AccessTokenInfoResponse | null>(null);
const [refreshToken, setRefreshToken] = useState('');
const [storedAuthUser, setStoredAuthUser] = useState<string | null>(null);

const handleRefresh = async () => {
try {
const refreshResponse = await refreshAuth({ refreshToken }).unwrap();
setResponse(refreshResponse);
setStoredAuthUser(localStorage.getItem(LS_KEYS.AUTH_USER));
} catch {
setResponse(null);
setStoredAuthUser(localStorage.getItem(LS_KEYS.AUTH_USER));
}
};

return (
<section>
<h2>Auth refresh test</h2>
<label htmlFor="refresh-token">Refresh token</label>
<input
id="refresh-token"
type="text"
required
value={refreshToken}
onChange={(event) => setRefreshToken(event.target.value)}
/>
<br />
<button
type="button"
onClick={handleRefresh}
disabled={isLoading || !refreshToken}
>
{isLoading ? 'Refreshing...' : 'Refresh auth'}
</button>
{error && <pre>{JSON.stringify(error, null, 2)}</pre>}
{response && <pre>{JSON.stringify(response, null, 2)}</pre>}
<br />
<h3>Redux auth state</h3>
<pre>{JSON.stringify(authUser, null, 2)}</pre>
<br />
<h3>localStorage AuthUser after request</h3>
<pre>{storedAuthUser ?? 'No localStorage snapshot yet'}</pre>
<br />
</section>
);
};

export default AuthRefreshTest;
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import NewsCardsGrid from './_components/NewsCardsGrid';
import DescriptionCardsSection from './_components/DescriptionCardsSection';
import MobileCardsGrid from './_components/MobileCardsGrid';
import BarIndicatorBlock from '@/app/[lng]/(helper)/admin/_components/BarIndicatorBlock';
import AuthRefreshTest from './_components/AuthRefreshTest';
import useSizes from '@/shared/lib/hooks/useSizes';
import hannu from '@/shared/assets/images/heros/hannu-hodari/hannu-hodari.png';
import vihapuhe from '@/shared/assets/images/heros/hate-speech/Vihapuhe.png';
Expand Down Expand Up @@ -48,6 +49,8 @@ const Page = () => {
component: <NavMenusBlock variant="forum" />,
}}
>
<AuthRefreshTest />

<button
style={{
paddingLeft: '1em',
Expand Down
2 changes: 2 additions & 0 deletions frontend-next-migration/src/entities/Auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type {
IUserRegisterDto,
IUserLoginDto,
IRefreshAuthDto,
IPlayerRegisterPartDto,
AuthUserSchema,
ILoginResponse,
Expand All @@ -11,6 +12,7 @@ export {
useLoginMutation,
useRegisterMutation,
useLogoutMutation,
useRefreshAuthMutation,
authEndpoints,
} from './model/authApi';

Expand Down
15 changes: 15 additions & 0 deletions frontend-next-migration/src/entities/Auth/middlewares/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Authentication refresh investigation

## Current implementation

`authApi.ts` exposes `refreshAuth` as a `POST /auth/refresh` mutation with the required `{ refreshToken }` JSON body. It returns `AccessTokenInfoResponse`, containing the new access and refresh tokens. The mutation is intentionally not called by production authentication code yet.

The admin page contains a manual probe at `/{lng}/admin`. It displays the refresh response, the current Redux auth state, and an `AuthUser` localStorage snapshot after the request. This makes it possible to verify the backend contract without changing the normal login flow.

## Middleware findings

The existing middleware can observe a successful RTK Query mutation with `authEndpoints.refreshAuth.matchFulfilled(action)`. A future integration could then dispatch `authUserActions.setAccessTokenInfo` with the refreshed token and its calculated expiry. The existing `setAccessTokenInfo` branch would persist that state to localStorage.

The middleware should not start the refresh request itself on every action. It is synchronous, and a request started from the middleware would need a separate expiry trigger, concurrency guard, and failure path. A `baseQuery` wrapper that retries an expired request once after dispatching `refreshAuth` is a better fit for automatic refresh. It must also prevent refresh loops and coordinate concurrent requests.

Before wiring this into production, verify the endpoint response and refresh-cookie behavior with the admin probe. If the response expiry field is not `accessTokenExpiresInSecIn`, update `AccessTokenInfoResponse` and the expiry mapping together.
13 changes: 13 additions & 0 deletions frontend-next-migration/src/entities/Auth/model/authApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { authEndpoints, useRefreshAuthMutation } from './authApi';

describe('authApi refresh endpoint', () => {
it('exposes the refreshAuth mutation and React hook', () => {
expect(authEndpoints.refreshAuth).toEqual(
expect.objectContaining({
initiate: expect.any(Function),
matchFulfilled: expect.any(Function),
}),
);
expect(useRefreshAuthMutation).toEqual(expect.any(Function));
});
});
16 changes: 15 additions & 1 deletion frontend-next-migration/src/entities/Auth/model/authApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { gameApi } from '@/shared/api';
import { IUserRegisterDto, IUserLoginDto, ILoginResponse } from '../types/authUser';
import {
IUserRegisterDto,
IUserLoginDto,
IRefreshAuthDto,
ILoginResponse,
AccessTokenInfoResponse,
} from '../types/authUser';

const authApi = gameApi.injectEndpoints({
endpoints: (builder) => ({
Expand All @@ -23,6 +29,13 @@ const authApi = gameApi.injectEndpoints({
method: 'POST',
}),
}),
refreshAuth: builder.mutation<AccessTokenInfoResponse, IRefreshAuthDto>({
query: (refreshAuthDto) => ({
url: '/auth/refresh',
method: 'POST',
body: refreshAuthDto,
}),
}),
}),
overrideExisting: false,
});
Expand All @@ -31,5 +44,6 @@ export const {
useLoginMutation,
useRegisterMutation,
useLogoutMutation,
useRefreshAuthMutation,
endpoints: authEndpoints,
} = authApi;
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@ export type IUserRegisterDto = Pick<IProfile<IPlayer>, 'username'> & {

export type IUserLoginDto = Pick<IProfile<IPlayer>, 'username'> & { password: string };

export type IRefreshAuthDto = {
refreshToken: string;
};

export type ILoginResponse = IProfile<IPlayer> & {
accessToken: string;
};

export type AccessTokenInfoResponse = {
accessToken: string;
refreshToken: string;
accessTokenExpiresInSecIn: number;
};

Expand Down
Loading