File size: 3,552 Bytes
788eab4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// 100% Offline HTTP Client with Automated CSRF Protection and Network Audit Tracker

interface RequestAuditLog {
  url: string;
  method: string;
  timestamp: string;
  isExternal: boolean;
  status: number;
}

class ApiAuditTracker {
  private logs: RequestAuditLog[] = [];
  private externalCount = 0;

  record(url: string, method: string, status: number) {
    const isExternal = !url.startsWith("/") && !url.startsWith("http://127.0.0.1") && !url.startsWith("http://localhost");
    if (isExternal) {
      this.externalCount++;
    }
    this.logs.unshift({
      url,
      method,
      timestamp: new Date().toISOString(),
      isExternal,
      status,
    });
    if (this.logs.length > 100) {
      this.logs.pop();
    }
  }

  getExternalCount(): number {
    return this.externalCount;
  }

  getLogs(): RequestAuditLog[] {
    return [...this.logs];
  }

  clear() {
    this.logs = [];
    this.externalCount = 0;
  }
}

export const auditTracker = new ApiAuditTracker();

let cachedCsrfToken: string | null = null;

export function setCsrfToken(token: string) {
  cachedCsrfToken = token;
}

export function getCsrfToken(): string | null {
  if (cachedCsrfToken) return cachedCsrfToken;
  const match = document.cookie.match(/(?:^|; )csrf_token=([^;]*)/);
  return match ? decodeURIComponent(match[1]) : null;
}

export async function request<T>(
  url: string,
  options: RequestInit = {}
): Promise<T> {
  const method = (options.method || "GET").toUpperCase();
  const headers = new Headers(options.headers || {});

  // Automatic CSRF token injection on mutating verbs
  if (["POST", "PUT", "DELETE", "PATCH"].includes(method)) {
    const token = getCsrfToken();
    if (token && !headers.has("X-CSRF-Token")) {
      headers.set("X-CSRF-Token", token);
    }
  }

  const response = await fetch(url, {
    ...options,
    headers,
    credentials: "include",
  });

  auditTracker.record(url, method, response.status);

  // Auto-refresh CSRF cookie if returned
  const returnedCsrf = response.headers.get("X-CSRF-Token");
  if (returnedCsrf) {
    cachedCsrfToken = returnedCsrf;
  }

  if (!response.ok) {
    let errorDetail = `Request failed with status ${response.status}`;
    try {
      const errJson = await response.json();
      errorDetail = errJson.detail || JSON.stringify(errJson);
    } catch {
      errorDetail = await response.text();
    }
    throw new Error(errorDetail);
  }

  // Handle empty responses
  const contentType = response.headers.get("content-type");
  if (contentType && contentType.includes("application/json")) {
    return (await response.json()) as T;
  }
  return (await response.text()) as unknown as T;
}

export const api = {
  get: <T>(url: string, signal?: AbortSignal) => request<T>(url, { method: "GET", signal }),
  post: <T>(url: string, body?: any, signal?: AbortSignal) => {
    const isFormData = body instanceof FormData;
    return request<T>(url, {
      method: "POST",
      headers: isFormData ? {} : { "Content-Type": "application/json" },
      body: isFormData ? body : JSON.stringify(body),
      signal,
    });
  },
  put: <T>(url: string, body?: any, signal?: AbortSignal) =>
    request<T>(url, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
      signal,
    }),
  delete: <T>(url: string, signal?: AbortSignal) => request<T>(url, { method: "DELETE", signal }),
};