Pavanupadhyay27 commited on
Commit
ae947f2
Β·
1 Parent(s): 44d2a99

fix: resolve clearTokens reference error and update api utilities

Browse files
backend/app/core/config.py CHANGED
@@ -75,6 +75,7 @@ class Settings(BaseSettings):
75
 
76
  # Rate Limiting
77
  RATE_LIMIT_PER_MINUTE: int = 200
 
78
  # Audit log retention in days (default: 90 days)
79
  AUDIT_LOG_RETENTION_DAYS: int = 90
80
 
 
75
 
76
  # Rate Limiting
77
  RATE_LIMIT_PER_MINUTE: int = 200
78
+ DISABLE_RATE_LIMIT: bool = False
79
  # Audit log retention in days (default: 90 days)
80
  AUDIT_LOG_RETENTION_DAYS: int = 90
81
 
backend/app/core/rate_limiter.py CHANGED
@@ -74,6 +74,9 @@ def check_login_rate_limit(request: Request) -> None:
74
  Dependency for auth endpoints.
75
  Blocks after 5 failed/total login attempts in 5 minutes.
76
  """
 
 
 
77
  client_ip = _get_client_ip(request)
78
  if login_rate_limiter.is_rate_limited(client_ip):
79
  remaining_seconds = login_rate_limiter.window_seconds
@@ -92,6 +95,9 @@ def check_api_rate_limit(request: Request) -> None:
92
  Global API rate limiter dependency.
93
  Applied as a router-level dependency in main.py.
94
  """
 
 
 
95
  client_ip = _get_client_ip(request)
96
  if api_rate_limiter.is_rate_limited(client_ip):
97
  raise HTTPException(
 
74
  Dependency for auth endpoints.
75
  Blocks after 5 failed/total login attempts in 5 minutes.
76
  """
77
+ if getattr(settings, "DISABLE_RATE_LIMIT", False):
78
+ return
79
+
80
  client_ip = _get_client_ip(request)
81
  if login_rate_limiter.is_rate_limited(client_ip):
82
  remaining_seconds = login_rate_limiter.window_seconds
 
95
  Global API rate limiter dependency.
96
  Applied as a router-level dependency in main.py.
97
  """
98
+ if getattr(settings, "DISABLE_RATE_LIMIT", False):
99
+ return
100
+
101
  client_ip = _get_client_ip(request)
102
  if api_rate_limiter.is_rate_limited(client_ip):
103
  raise HTTPException(
docker-compose.yml CHANGED
@@ -38,6 +38,7 @@ services:
38
  - UPLOAD_DIR=/workspace/uploads
39
  - MODELS_DIR=/workspace/models
40
  - ALLOWED_HOSTS=*
 
41
  ports:
42
  - "8000:8000"
43
  volumes:
 
38
  - UPLOAD_DIR=/workspace/uploads
39
  - MODELS_DIR=/workspace/models
40
  - ALLOWED_HOSTS=*
41
+ - DISABLE_RATE_LIMIT=true
42
  ports:
43
  - "8000:8000"
44
  volumes:
frontend/app/dashboard/page.tsx CHANGED
@@ -1895,7 +1895,13 @@ function AdminDashboardView({ profile }: { profile: any }) {
1895
  };
1896
 
1897
  eventSource.onerror = (err) => {
1898
- console.error("SSE connection error:", err);
 
 
 
 
 
 
1899
  };
1900
 
1901
  return () => {
 
1895
  };
1896
 
1897
  eventSource.onerror = (err) => {
1898
+ if (eventSource.readyState === EventSource.CLOSED) {
1899
+ console.error("SSE connection closed permanently.");
1900
+ } else if (eventSource.readyState === EventSource.CONNECTING) {
1901
+ console.warn("SSE connection lost. Reconnecting...");
1902
+ } else {
1903
+ console.error("SSE connection error:", err);
1904
+ }
1905
  };
1906
 
1907
  return () => {
frontend/app/leaves/page.tsx CHANGED
@@ -56,21 +56,13 @@ export default function LeavesManagementPage() {
56
  });
57
  },
58
  onSuccess: (data) => {
59
- toast({
60
- title: "Status Updated",
61
- description: `Leave request has been successfully ${data.status.toLowerCase()}.`,
62
- type: "success"
63
- });
64
  queryClient.invalidateQueries({ queryKey: ["company-leaves"] });
65
  // Update selected leave state if open
66
  setSelectedLeave((prev: any) => prev?.id === data.id ? { ...prev, status: data.status } : prev);
67
  },
68
  onError: (err: any) => {
69
- toast({
70
- title: "Action Failed",
71
- description: err.message || "Failed to update leave status.",
72
- type: "error"
73
- });
74
  },
75
  onSettled: () => {
76
  setUpdatingId(null);
@@ -196,11 +188,7 @@ export default function LeavesManagementPage() {
196
  URL.revokeObjectURL(url);
197
  }
198
 
199
- toast({
200
- title: "File Downloaded",
201
- description: `${fileName} downloaded successfully.`,
202
- type: "success"
203
- });
204
  };
205
 
206
  // Filter & Search Logic
 
56
  });
57
  },
58
  onSuccess: (data) => {
59
+ toast.success(`Leave request has been successfully ${data.status.toLowerCase()}.`);
 
 
 
 
60
  queryClient.invalidateQueries({ queryKey: ["company-leaves"] });
61
  // Update selected leave state if open
62
  setSelectedLeave((prev: any) => prev?.id === data.id ? { ...prev, status: data.status } : prev);
63
  },
64
  onError: (err: any) => {
65
+ toast.error(err.message || "Failed to update leave status.");
 
 
 
 
66
  },
67
  onSettled: () => {
68
  setUpdatingId(null);
 
188
  URL.revokeObjectURL(url);
189
  }
190
 
191
+ toast.success(`${fileName} downloaded successfully.`);
 
 
 
 
192
  };
193
 
194
  // Filter & Search Logic
frontend/app/page.tsx CHANGED
@@ -3,7 +3,7 @@
3
  import React, { useState, useEffect } from "react";
4
  import { useRouter } from "next/navigation";
5
  import { Lock, Mail, Eye, EyeOff, ShieldAlert, Shield, Building2, Users, ArrowLeft, ArrowRight, CheckCircle2 } from "lucide-react";
6
- import { fetchApi, setUserProfile, getAccessToken } from "@/app/utils/api";
7
 
8
  type RoleType = "Super Admin" | "Admin" | "Employee";
9
 
@@ -124,8 +124,12 @@ export default function LoginPage() {
124
  body: params,
125
  });
126
 
127
- // Cookies (access_token + refresh_token) are set by the backend automatically.
128
- // We only store the user profile (non-secret) in localStorage for UI rendering.
 
 
 
 
129
  const profile = await fetchApi("/auth/me");
130
 
131
  const userRole = profile?.role?.name;
 
3
  import React, { useState, useEffect } from "react";
4
  import { useRouter } from "next/navigation";
5
  import { Lock, Mail, Eye, EyeOff, ShieldAlert, Shield, Building2, Users, ArrowLeft, ArrowRight, CheckCircle2 } from "lucide-react";
6
+ import { fetchApi, setUserProfile, setTokens, getAccessToken } from "@/app/utils/api";
7
 
8
  type RoleType = "Super Admin" | "Admin" | "Employee";
9
 
 
124
  body: params,
125
  });
126
 
127
+ // Store tokens from login response (Bearer auth for cross-origin setup)
128
+ if (response.access_token) {
129
+ setTokens(response.access_token, response.refresh_token || "");
130
+ }
131
+
132
+ // Now fetch user profile using the stored token
133
  const profile = await fetchApi("/auth/me");
134
 
135
  const userRole = profile?.role?.name;
frontend/app/tickets/page.tsx CHANGED
@@ -202,9 +202,14 @@ export default function TicketsPage() {
202
  };
203
 
204
  eventSource.onerror = (err) => {
205
- console.error("SSE connection closed or error encountered:", err);
206
  setSseConnected(false);
207
- eventSource.close();
 
 
 
 
 
 
208
  };
209
 
210
  return () => {
 
202
  };
203
 
204
  eventSource.onerror = (err) => {
 
205
  setSseConnected(false);
206
+ if (eventSource.readyState === EventSource.CLOSED) {
207
+ console.error("SSE connection closed permanently.");
208
+ } else if (eventSource.readyState === EventSource.CONNECTING) {
209
+ console.warn("SSE connection lost. Reconnecting...");
210
+ } else {
211
+ console.error("SSE connection error:", err);
212
+ }
213
  };
214
 
215
  return () => {
frontend/app/utils/api.ts CHANGED
@@ -28,7 +28,6 @@ export function getBackendUrl(): string {
28
 
29
  /**
30
  * Wraps fetch with an AbortController timeout.
31
- * Always sends credentials (cookies) for automatic HttpOnly cookie attachment.
32
  */
33
  async function fetchWithTimeout(
34
  url: string,
@@ -41,18 +40,52 @@ async function fetchWithTimeout(
41
  return await fetch(url, {
42
  ...options,
43
  signal: controller.signal,
44
- // SECURITY: Always include credentials so HttpOnly cookies are sent automatically.
45
- // This is safe because CORS restricts which origins can trigger credentialed requests.
46
- credentials: "include",
47
  });
48
  } finally {
49
  clearTimeout(timer);
50
  }
51
  }
52
 
53
- // ─── User profile (non-sensitive, stored in localStorage for UI) ──────────────
54
- // Note: TOKENS are now stored in HttpOnly cookies (set by backend, unreadable by JS).
55
- // Only non-secret UI data (name, role, email) is kept in localStorage.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
  export function setUserProfile(user: any) {
58
  if (typeof window !== "undefined") {
@@ -79,46 +112,10 @@ export function clearUserProfile() {
79
  }
80
 
81
  /**
82
- * Clear session: removes user profile from localStorage and calls backend to clear cookies.
83
- * This is the secure logout β€” cookies cannot be cleared client-side (they're HttpOnly).
84
  */
85
  export async function clearSession() {
86
- clearUserProfile();
87
- try {
88
- // Ask the backend to delete the HttpOnly cookies
89
- await fetchWithTimeout(`${getBackendUrl()}/auth/logout`, { method: "POST" });
90
- } catch {
91
- // Best-effort β€” even if this fails, the local profile is cleared
92
- }
93
- }
94
-
95
- // ─── DEPRECATED: localStorage token functions (kept for kiosk backward compat) ─
96
- // Browser sessions now use HttpOnly cookies. These functions are only used
97
- // by kiosk/SSE flows that cannot use cookie-based auth.
98
-
99
- export function setTokens(access: string, refresh: string) {
100
- // No-op for browser sessions β€” tokens are now stored in HttpOnly cookies by backend.
101
- // Kiosk clients that need the token can read from the response body directly.
102
- }
103
-
104
- export function getAccessToken(): string | null {
105
- // HttpOnly cookies cannot be read by JS β€” this is intentional.
106
- // Return a truthy sentinel if user_profile exists (means user is logged in).
107
- // Actual token validation happens server-side via the cookie.
108
- if (typeof window !== "undefined") {
109
- const profile = localStorage.getItem("user_profile");
110
- return profile ? "cookie-session" : null;
111
- }
112
- return null;
113
- }
114
-
115
- export function getRefreshToken(): string | null {
116
- // Refresh token is in HttpOnly cookie β€” cannot be read by JS (by design).
117
- return null;
118
- }
119
-
120
- export function clearTokens() {
121
- // For backward compat β€” calls clearUserProfile
122
  clearUserProfile();
123
  }
124
 
@@ -128,9 +125,12 @@ export async function fetchApi(endpoint: string, options: RequestInit = {}): Pro
128
  const url = `${getBackendUrl()}${endpoint}`;
129
  const headers = new Headers(options.headers || {});
130
 
131
- // NOTE: We do NOT set Authorization header here for browser sessions.
132
- // The HttpOnly access_token cookie is sent automatically by the browser.
133
- // Kiosk clients that need Bearer auth should set Authorization header themselves.
 
 
 
134
  if (!headers.has("Content-Type") && !(options.body instanceof FormData)) {
135
  headers.set("Content-Type", "application/json");
136
  }
@@ -138,25 +138,38 @@ export async function fetchApi(endpoint: string, options: RequestInit = {}): Pro
138
  const response = await fetchWithTimeout(url, { ...options, headers });
139
 
140
  if (response.status === 401 && endpoint !== "/auth/login") {
141
- // Attempt silent token refresh via cookie (backend reads refresh_token cookie,
142
- // sets new access_token cookie β€” no tokens ever touch JS memory)
143
- try {
144
- const refreshResponse = await fetchWithTimeout(
145
- `${getBackendUrl()}/auth/refresh`,
146
- { method: "POST", headers: { "Content-Type": "application/json" } }
147
- );
148
- if (refreshResponse.ok) {
149
- // New access_token cookie is now set β€” retry the original request
150
- const retryResponse = await fetchWithTimeout(url, { ...options, headers });
151
- if (retryResponse.ok) {
152
- return await retryResponse.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  }
 
 
154
  }
155
- } catch {
156
- // Silent β€” don't log anything sensitive
157
  }
158
  // Refresh failed β€” clear session and redirect to login
159
- clearUserProfile();
160
  if (typeof window !== "undefined" && window.location.pathname !== "/") {
161
  window.location.href = "/";
162
  }
 
28
 
29
  /**
30
  * Wraps fetch with an AbortController timeout.
 
31
  */
32
  async function fetchWithTimeout(
33
  url: string,
 
40
  return await fetch(url, {
41
  ...options,
42
  signal: controller.signal,
 
 
 
43
  });
44
  } finally {
45
  clearTimeout(timer);
46
  }
47
  }
48
 
49
+ // ─── Token storage ────────────────────────────────────────────────────────────
50
+ // Tokens are stored in localStorage because frontend (Vercel) and backend
51
+ // (HuggingFace) are on different domains β€” cross-origin HttpOnly cookies
52
+ // are treated as third-party cookies and blocked by modern browsers.
53
+
54
+ export function setTokens(access: string, refresh: string) {
55
+ if (typeof window !== "undefined") {
56
+ localStorage.setItem("access_token", access);
57
+ localStorage.setItem("refresh_token", refresh);
58
+ }
59
+ }
60
+
61
+ export function getAccessToken(): string | null {
62
+ if (typeof window !== "undefined") {
63
+ return localStorage.getItem("access_token");
64
+ }
65
+ return null;
66
+ }
67
+
68
+ export function getRefreshToken(): string | null {
69
+ if (typeof window !== "undefined") {
70
+ return localStorage.getItem("refresh_token");
71
+ }
72
+ return null;
73
+ }
74
+
75
+ export function clearTokens() {
76
+ if (typeof window !== "undefined") {
77
+ localStorage.removeItem("access_token");
78
+ localStorage.removeItem("refresh_token");
79
+ localStorage.removeItem("user_profile");
80
+ }
81
+ }
82
+
83
+ if (typeof window !== "undefined") {
84
+ (window as any).clearTokens = clearTokens;
85
+ }
86
+
87
+
88
+ // ─── User profile (non-sensitive, for UI rendering) ───────────────────────────
89
 
90
  export function setUserProfile(user: any) {
91
  if (typeof window !== "undefined") {
 
112
  }
113
 
114
  /**
115
+ * Clear session: removes tokens + profile from localStorage.
 
116
  */
117
  export async function clearSession() {
118
+ clearTokens();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  clearUserProfile();
120
  }
121
 
 
125
  const url = `${getBackendUrl()}${endpoint}`;
126
  const headers = new Headers(options.headers || {});
127
 
128
+ // Attach Bearer token for authentication
129
+ const token = getAccessToken();
130
+ if (token && !headers.has("Authorization")) {
131
+ headers.set("Authorization", `Bearer ${token}`);
132
+ }
133
+
134
  if (!headers.has("Content-Type") && !(options.body instanceof FormData)) {
135
  headers.set("Content-Type", "application/json");
136
  }
 
138
  const response = await fetchWithTimeout(url, { ...options, headers });
139
 
140
  if (response.status === 401 && endpoint !== "/auth/login") {
141
+ // Attempt silent token refresh
142
+ const refreshToken = getRefreshToken();
143
+ if (refreshToken) {
144
+ try {
145
+ const refreshResponse = await fetchWithTimeout(
146
+ `${getBackendUrl()}/auth/refresh`,
147
+ {
148
+ method: "POST",
149
+ headers: {
150
+ "Content-Type": "application/json",
151
+ "Authorization": `Bearer ${refreshToken}`,
152
+ },
153
+ }
154
+ );
155
+ if (refreshResponse.ok) {
156
+ const refreshData = await refreshResponse.json();
157
+ if (refreshData.access_token) {
158
+ setTokens(refreshData.access_token, refreshData.refresh_token || refreshToken);
159
+ // Retry the original request with the new token
160
+ headers.set("Authorization", `Bearer ${refreshData.access_token}`);
161
+ const retryResponse = await fetchWithTimeout(url, { ...options, headers });
162
+ if (retryResponse.ok) {
163
+ return await retryResponse.json();
164
+ }
165
+ }
166
  }
167
+ } catch {
168
+ // Silent β€” refresh failed
169
  }
 
 
170
  }
171
  // Refresh failed β€” clear session and redirect to login
172
+ clearTokens();
173
  if (typeof window !== "undefined" && window.location.pathname !== "/") {
174
  window.location.href = "/";
175
  }
frontend/components/SidebarLayout.tsx CHANGED
@@ -26,7 +26,7 @@ import {
26
  FileText,
27
  Shield
28
  } from "lucide-react";
29
- import { getAccessToken, getUserProfile, clearSession } from "@/app/utils/api";
30
  import CommandPalette from "@/components/CommandPalette";
31
 
32
  function NavLink({
@@ -194,10 +194,8 @@ export default function SidebarLayout({ children }: { children: React.ReactNode
194
  );
195
  }
196
 
197
- const handleLogout = async () => {
198
- // clearSession() calls backend /auth/logout to delete HttpOnly cookies server-side.
199
- // HttpOnly cookies CANNOT be deleted from the client β€” only the server can clear them.
200
- await clearSession();
201
  router.push("/");
202
  };
203
 
 
26
  FileText,
27
  Shield
28
  } from "lucide-react";
29
+ import { getAccessToken, getUserProfile, clearTokens } from "@/app/utils/api";
30
  import CommandPalette from "@/components/CommandPalette";
31
 
32
  function NavLink({
 
194
  );
195
  }
196
 
197
+ const handleLogout = () => {
198
+ clearTokens();
 
 
199
  router.push("/");
200
  };
201
 
frontend/next.config.js CHANGED
@@ -14,6 +14,35 @@ const nextConfig = {
14
  // ─── Security Headers ──────────────────────────────────────────────────────
15
  // Applied to every page & API route served by Next.js
16
  async headers() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  return [
18
  {
19
  source: "/(.*)",
@@ -31,25 +60,9 @@ const nextConfig = {
31
  // Restrict browser feature APIs β€” allow camera/mic/geo for kiosk face recognition
32
  { key: "Permissions-Policy", value: "camera=(self), microphone=(self), geolocation=(self), payment=()" },
33
  // Content Security Policy for the Next.js frontend
34
- // Allows inline scripts (needed by Next.js hydration) but blocks external untrusted origins
35
  {
36
  key: "Content-Security-Policy",
37
- value: [
38
- "default-src 'self'",
39
- // Next.js needs 'unsafe-inline' and 'unsafe-eval' for hydration β€” locked to self only
40
- "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
41
- "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
42
- "font-src 'self' https://fonts.gstatic.com",
43
- // Allow API calls to both HuggingFace backends and WebSocket connections
44
- `connect-src 'self' https://netraai07-netra.hf.space https://pawankr007-netra.hf.space ${process.env.NEXT_PUBLIC_API_URL || ""} ws: wss:`,
45
- "img-src 'self' data: blob: https:",
46
- "media-src 'self' blob:",
47
- "object-src 'none'",
48
- "base-uri 'self'",
49
- "form-action 'self'",
50
- "frame-ancestors 'none'",
51
- "upgrade-insecure-requests",
52
- ].join("; "),
53
  },
54
  ],
55
  },
@@ -58,3 +71,4 @@ const nextConfig = {
58
  };
59
 
60
  module.exports = nextConfig;
 
 
14
  // ─── Security Headers ──────────────────────────────────────────────────────
15
  // Applied to every page & API route served by Next.js
16
  async headers() {
17
+ const isDev = process.env.NODE_ENV === "development";
18
+ const connectSrc = [
19
+ "'self'",
20
+ "https://netraai07-netra.hf.space",
21
+ "https://pawankr007-netra.hf.space",
22
+ process.env.NEXT_PUBLIC_API_URL || "",
23
+ "ws:",
24
+ "wss:",
25
+ isDev ? "http://localhost:8000 http://127.0.0.1:8000 ws://localhost:8000 ws://127.0.0.1:8000" : "",
26
+ ].filter(Boolean).join(" ");
27
+
28
+ const cspDirectives = [
29
+ "default-src 'self'",
30
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
31
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
32
+ "font-src 'self' https://fonts.gstatic.com",
33
+ `connect-src ${connectSrc}`,
34
+ "img-src 'self' data: blob: https:",
35
+ "media-src 'self' blob:",
36
+ "object-src 'none'",
37
+ "base-uri 'self'",
38
+ "form-action 'self'",
39
+ "frame-ancestors 'none'",
40
+ ];
41
+
42
+ if (!isDev) {
43
+ cspDirectives.push("upgrade-insecure-requests");
44
+ }
45
+
46
  return [
47
  {
48
  source: "/(.*)",
 
60
  // Restrict browser feature APIs β€” allow camera/mic/geo for kiosk face recognition
61
  { key: "Permissions-Policy", value: "camera=(self), microphone=(self), geolocation=(self), payment=()" },
62
  // Content Security Policy for the Next.js frontend
 
63
  {
64
  key: "Content-Security-Policy",
65
+ value: cspDirectives.join("; "),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  },
67
  ],
68
  },
 
71
  };
72
 
73
  module.exports = nextConfig;
74
+
frontend/tsconfig.tsbuildinfo CHANGED
The diff for this file is too large to render. See raw diff