Compare commits

...

2 Commits

Author SHA1 Message Date
c1cffbcafa fix auth refresh endpoint and quiet expected 401 on profile check 2026-06-26 13:23:03 +03:30
6738ae225d Merge pull request 'bugfix/tab-size-issue' (#49) from bugfix/tab-size-issue into master
All checks were successful
Registry — build, push, deploy / temp-success (push) Successful in 1s
Reviewed-on: http://178.131.50.201:3000/admin/dyolink/pulls/49
Reviewed-by: admin <admin@localhost>
2026-06-25 11:56:47 +03:30
3 changed files with 52 additions and 3 deletions

View File

@@ -11,6 +11,7 @@ import {
HttpStatus, HttpStatus,
Get, Get,
Patch, Patch,
UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import type { Response } from 'express'; import type { Response } from 'express';
import { import {
@@ -173,6 +174,33 @@ export class AuthController {
); );
} }
// =========================
// REFRESH
// =========================
@Post('refresh')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Refresh access token using refresh cookie' })
@ApiResponse({ status: 200, description: 'Access token refreshed' })
@ApiUnauthorizedResponse({ description: 'Invalid or missing refresh token' })
async refresh(@Req() req, @Res({ passthrough: true }) res: Response) {
const refreshToken = req?.cookies?.refreshToken;
if (!refreshToken) {
throw new UnauthorizedException('Refresh token not found');
}
const result = await this.authService.refreshToken(refreshToken);
this.setAccessToken(res, result.data.accessToken);
return {
success: true,
data: {
accessToken: result.data.accessToken,
},
};
}
// ========================= // =========================
// LOGOUT // LOGOUT
// ========================= // =========================

View File

@@ -26,6 +26,18 @@ function isPublicInvitationRequest(url: string | undefined): boolean {
); );
} }
/** Session bootstrap / auth endpoints where 401 means "not logged in", not "retry refresh". */
function shouldSkipRefreshRetry(url: string | undefined): boolean {
if (!url) return false;
return (
url.includes('/auth/profile') ||
url.includes('/auth/refresh') ||
url.includes('/auth/login') ||
url.includes('/auth/register') ||
url.includes('/auth/logout')
);
}
// ❌ REMOVE request interceptor completely (no Authorization header) // ❌ REMOVE request interceptor completely (no Authorization header)
// ✅ Response interceptor // ✅ Response interceptor
@@ -37,7 +49,8 @@ apiClient.interceptors.response.use(
if ( if (
error.response?.status === 401 && error.response?.status === 401 &&
!originalRequest._retry && !originalRequest._retry &&
!isPublicInvitationRequest(originalRequest.url) !isPublicInvitationRequest(originalRequest.url) &&
!shouldSkipRefreshRetry(originalRequest.url)
) { ) {
originalRequest._retry = true; originalRequest._retry = true;

View File

@@ -107,8 +107,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setCurrentOrganization(null); setCurrentOrganization(null);
} }
} }
} catch (err) { } catch (err: unknown) {
console.error('Auth check failed:', err); const status =
(err as { statusCode?: number })?.statusCode ??
(err as { response?: { status?: number } })?.response?.status;
// 401 on profile is expected when there is no session — not an application error.
if (status !== 401) {
console.error('Auth check failed:', err);
}
// Only clear state — DO NOT redirect here // Only clear state — DO NOT redirect here
setUser(null); setUser(null);
setOrganizations([]); setOrganizations([]);