Repository navigation
Expand file tree
/
Copy pathacled.js
More file actions
156 lines (136 loc) · 4.03 KB
/
Copy pathacled.js
File metadata and controls
156 lines (136 loc) · 4.03 KB
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// ACLED API proxy - keeps token server-side only
// Token is stored in ACLED_ACCESS_TOKEN (no VITE_ prefix)
export const config = { runtime: 'edge' };
// In-memory cache (edge function instance)
let cache = { data: null, timestamp: 0 };
const CACHE_TTL = 10 * 60 * 1000; // 10 minutes
// Rate limiting - track requests per IP
const rateLimits = new Map();
const RATE_LIMIT = 10; // requests per minute
const RATE_WINDOW = 60 * 1000;
function checkRateLimit(ip) {
const now = Date.now();
const record = rateLimits.get(ip);
if (!record || now - record.windowStart > RATE_WINDOW) {
rateLimits.set(ip, { count: 1, windowStart: now });
return true;
}
if (record.count >= RATE_LIMIT) {
return false;
}
record.count++;
return true;
}
export default async function handler(req) {
// Get client IP for rate limiting
const ip = req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'unknown';
// Check rate limit
if (!checkRateLimit(ip)) {
return Response.json({ error: 'Rate limited', data: [] }, {
status: 429,
headers: {
'Access-Control-Allow-Origin': '*',
'Retry-After': '60',
},
});
}
// Get token from server-side env (no VITE_ prefix)
const token = process.env.ACLED_ACCESS_TOKEN;
if (!token) {
return Response.json({
error: 'ACLED not configured',
data: [],
configured: false
}, {
status: 200,
headers: { 'Access-Control-Allow-Origin': '*' },
});
}
// Check cache
const now = Date.now();
if (cache.data && now - cache.timestamp < CACHE_TTL) {
return Response.json(cache.data, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=300',
'X-Cache': 'HIT',
},
});
}
try {
// Calculate date range (last 30 days)
const endDate = new Date().toISOString().split('T')[0];
const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
const params = new URLSearchParams({
event_type: 'Protests',
event_date: `${startDate}|${endDate}`,
event_date_where: 'BETWEEN',
limit: '500',
_format: 'json',
});
const response = await fetch(`https://acleddata.com/api/acled/read?${params}`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
},
});
if (!response.ok) {
const text = await response.text();
return Response.json({
error: `ACLED API error: ${response.status}`,
details: text.substring(0, 200),
data: [],
}, {
status: response.status,
headers: { 'Access-Control-Allow-Origin': '*' },
});
}
const rawData = await response.json();
const events = rawData.data || [];
// Return only needed fields to reduce payload and protect any sensitive data
const sanitizedEvents = events.map(e => ({
event_id_cnty: e.event_id_cnty,
event_date: e.event_date,
event_type: e.event_type,
sub_event_type: e.sub_event_type,
actor1: e.actor1,
actor2: e.actor2,
country: e.country,
admin1: e.admin1,
location: e.location,
latitude: e.latitude,
longitude: e.longitude,
fatalities: e.fatalities,
notes: e.notes?.substring(0, 500), // Truncate long notes
source: e.source,
tags: e.tags,
}));
const result = {
success: true,
count: sanitizedEvents.length,
data: sanitizedEvents,
cached_at: new Date().toISOString(),
};
// Update cache
cache = { data: result, timestamp: now };
return Response.json(result, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=300',
'X-Cache': 'MISS',
},
});
} catch (error) {
return Response.json({
error: `Fetch failed: ${error.message}`,
data: [],
}, {
status: 500,
headers: { 'Access-Control-Allow-Origin': '*' },
});
}
}