Spaces:
Sleeping
Sleeping
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 +1 -0
- backend/app/core/rate_limiter.py +6 -0
- docker-compose.yml +1 -0
- frontend/app/dashboard/page.tsx +7 -1
- frontend/app/leaves/page.tsx +3 -15
- frontend/app/page.tsx +7 -3
- frontend/app/tickets/page.tsx +7 -2
- frontend/app/utils/api.ts +76 -63
- frontend/components/SidebarLayout.tsx +3 -5
- frontend/next.config.js +31 -17
- frontend/tsconfig.tsbuildinfo +0 -0
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
// βββ
|
| 54 |
-
//
|
| 55 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 83 |
-
* This is the secure logout β cookies cannot be cleared client-side (they're HttpOnly).
|
| 84 |
*/
|
| 85 |
export async function clearSession() {
|
| 86 |
-
|
| 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 |
-
//
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
}
|
|
|
|
|
|
|
| 154 |
}
|
| 155 |
-
} catch {
|
| 156 |
-
// Silent β don't log anything sensitive
|
| 157 |
}
|
| 158 |
// Refresh failed β clear session and redirect to login
|
| 159 |
-
|
| 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,
|
| 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 =
|
| 198 |
-
|
| 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
|
|
|