Compare commits

...

5 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
db4803d463 fix sidebar width change during toggling between tabs. 2026-06-24 17:09:06 +03:30
740f6d9cc9 test disable push on master 2026-06-23 15:54:13 +03:30
36cf991684 Merge pull request 'feature/tab-warning-flag' (#44) from feature/tab-warning-flag into master
All checks were successful
Registry — build, push, deploy / temp-success (push) Successful in 0s
Reviewed-on: http://178.131.50.201:3000/admin/dyolink/pulls/44
2026-06-22 14:44:19 +03:30
8 changed files with 57 additions and 8 deletions

View File

@@ -1,6 +1,6 @@
# Dyolink
Monorepo: **NestJS** backend (`backend/`), **Next.js** frontend (`frontend/`), **Docker** stack under `infrastructure/`.
Monorepo: **NestJS** backend (`backend/`), **Next.js** frontend (`frontend/`), **Docker** stack under `infrastructure/`..
Local development: see **`backend/README.md`** and **`frontend/README.md`**.

View File

@@ -11,6 +11,7 @@ import {
HttpStatus,
Get,
Patch,
UnauthorizedException,
} from '@nestjs/common';
import type { Response } from 'express';
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
// =========================

View File

@@ -53,7 +53,7 @@ export function InvitationHistoryDialog({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
<div
className="w-full max-w-[min(56rem,calc(100vw-17rem))] max-h-[90vh] overflow-y-auto rounded-[var(--radius-md)] border border-border bg-background-secondary p-6 shadow-xl space-y-4"
className="w-full max-w-[min(56rem,calc(100vw-15rem))] max-h-[90vh] overflow-y-auto rounded-[var(--radius-md)] border border-border bg-background-secondary p-6 shadow-xl space-y-4"
role="dialog"
aria-modal="true"
aria-labelledby="invitation-history-title"

View File

@@ -107,7 +107,7 @@ export function CreatePatientModal({
}}
>
<div
className="surface-card w-full max-w-[min(56rem,calc(100vw-17rem))] p-5 space-y-4 shadow-xl"
className="surface-card w-full max-w-[min(56rem,calc(100vw-15rem))] p-5 space-y-4 shadow-xl"
role="dialog"
aria-modal="true"
aria-labelledby="create-patient-dialog-title"

View File

@@ -59,7 +59,7 @@ function Sidebar() {
);
return (
<aside className="w-64 bg-background-secondary/90 border-r border-border text-text-primary flex flex-col">
<aside className="w-56 min-w-56 shrink-0 bg-background-secondary/90 border-r border-border text-text-primary flex flex-col">
<div className="h-[71px] px-4 flex items-center">
<h1 className="text-lg font-medium tracking-tight">{tCommon('appName')}</h1>
</div>

View File

@@ -66,7 +66,7 @@ export function TreatmentPreviewDialog({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
<div
className="w-full max-w-[min(56rem,calc(100vw-17rem))] max-h-[90vh] overflow-y-auto rounded-[var(--radius-md)] border border-border bg-background-secondary p-6 shadow-xl space-y-4"
className="w-full max-w-[min(56rem,calc(100vw-15rem))] max-h-[90vh] overflow-y-auto rounded-[var(--radius-md)] border border-border bg-background-secondary p-6 shadow-xl space-y-4"
role="dialog"
aria-modal="true"
aria-labelledby="treatment-preview-title"

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)
// ✅ Response interceptor
@@ -37,7 +49,8 @@ apiClient.interceptors.response.use(
if (
error.response?.status === 401 &&
!originalRequest._retry &&
!isPublicInvitationRequest(originalRequest.url)
!isPublicInvitationRequest(originalRequest.url) &&
!shouldSkipRefreshRetry(originalRequest.url)
) {
originalRequest._retry = true;

View File

@@ -107,8 +107,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setCurrentOrganization(null);
}
}
} catch (err) {
} catch (err: unknown) {
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
setUser(null);
setOrganizations([]);