-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathapi.js
236 lines (201 loc) · 6.39 KB
/
api.js
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
const express = require("express");
const cors = require("cors");
const path = require("path");
const fs = require("fs");
const rateLimit = require("express-rate-limit");
const app = express();
// Trust the proxy to get the real client IP
app.set("trust proxy", 1);
// Enable CORS for all routes
app.use(cors());
// Rate limiter configuration
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
standardHeaders: true,
legacyHeaders: false,
message: {
error: "Too many requests, please try again later.",
},
});
// Apply rate limiting to all API routes
app.use("/api/", apiLimiter);
// Load quotes data
const quotesData = JSON.parse(
fs.readFileSync(path.join(__dirname, "quotes.json"), "utf8"),
);
// Helper function to normalize author names
function normalizeAuthorName(author) {
return decodeURIComponent(author).trim().toLowerCase();
}
// Helper function to check if a quote's author matches any of the requested authors
function hasMatchingAuthor(quote, requestedAuthors) {
if (!requestedAuthors) return true;
const quoteAuthor = normalizeAuthorName(quote.author);
return requestedAuthors.some(
(author) => normalizeAuthorName(author) === quoteAuthor,
);
}
// Helper function to check if a quote matches all requested tags
function hasMatchingTags(quote, requestedTags) {
if (!requestedTags) return true;
const quoteTags = new Set(quote.tags);
return requestedTags.every((tag) => quoteTags.has(tag));
}
// Main quote retrieval function
function getQuotes({
maxLength = null,
minLength = null,
tags = null,
count = 1,
authors = null,
} = {}) {
let validQuotes = [...quotesData];
// Filter by authors if provided
if (authors) {
validQuotes = validQuotes.filter((quote) =>
hasMatchingAuthor(quote, authors),
);
}
// Filter by tags if provided
if (tags) {
validQuotes = validQuotes.filter((quote) => hasMatchingTags(quote, tags));
}
// If no quotes match the criteria, return null
if (validQuotes.length === 0) {
return null;
}
// Apply length filters
if (minLength !== null) {
validQuotes = validQuotes.filter((quote) => quote.length >= minLength);
}
if (maxLength !== null) {
validQuotes = validQuotes.filter((quote) => quote.length <= maxLength);
}
if (validQuotes.length === 0) {
return null;
}
// If requesting more quotes than available, return all available quotes
count = Math.min(count, validQuotes.length);
// Get random quotes
const quotes = [];
const tempQuotes = [...validQuotes];
for (let i = 0; i < count; i++) {
const randomIndex = Math.floor(Math.random() * tempQuotes.length);
quotes.push(tempQuotes[randomIndex]);
tempQuotes.splice(randomIndex, 1);
}
return quotes;
}
// Get list of all available authors with their quote counts
app.get("/api/authors", (req, res) => {
try {
const authorsData = JSON.parse(
fs.readFileSync(path.join(__dirname, "authors.json"), "utf8"),
);
res.json(authorsData);
} catch (error) {
res.status(500).json({
error: "Error fetching authors list",
});
}
});
// Get list of all available tags
app.get("/api/tags", (req, res) => {
try {
const tags = JSON.parse(
fs.readFileSync(path.join(__dirname, "tags.json"), "utf8"),
);
res.json(tags);
} catch (error) {
res.status(500).json({
error: "Error fetching tags list",
});
}
});
// Main quote endpoint
app.get("/api/quotes/random", (req, res) => {
const maxLength = req.query.maxLength ? parseInt(req.query.maxLength) : null;
const minLength = req.query.minLength ? parseInt(req.query.minLength) : null;
const tags = req.query.tags
? req.query.tags.split(",").map((tag) => tag.toLowerCase())
: null;
const authors = req.query.authors ? req.query.authors.split(",") : null;
const count = req.query.count ? parseInt(req.query.count) : 1;
// Validate count parameter
if (isNaN(count) || count < 1 || count > 50) {
return res.status(400).json({
error: "Count must be a number between 1 and 50.",
});
}
// Validate authors only if authors parameter is provided
if (authors) {
try {
const authorsData = JSON.parse(
fs.readFileSync(path.join(__dirname, "authors.json"), "utf8"),
);
// Create a map of lowercase author names to their proper case versions
const authorMap = {};
Object.keys(authorsData).forEach((author) => {
authorMap[author.toLowerCase()] = author;
});
// Check for invalid authors and convert to proper case
const processedAuthors = [];
const invalidAuthors = [];
authors.forEach((author) => {
const lowercaseAuthor = author.toLowerCase();
if (authorMap[lowercaseAuthor]) {
processedAuthors.push(authorMap[lowercaseAuthor]);
} else {
invalidAuthors.push(author);
}
});
if (invalidAuthors.length > 0) {
return res.status(400).json({
error: `Invalid author(s): ${invalidAuthors.join(", ")}`,
});
}
// Replace the authors array with the properly cased versions
authors.splice(0, authors.length, ...processedAuthors);
} catch (error) {
return res.status(500).json({
error: "Error validating authors",
});
}
}
// Validate tags only if tags parameter is provided
if (tags) {
try {
const validTags = new Set(
JSON.parse(fs.readFileSync(path.join(__dirname, "tags.json"), "utf8")),
);
const invalidTags = tags.filter((tag) => !validTags.has(tag));
if (invalidTags.length > 0) {
return res.status(400).json({
error: `Invalid tag(s): ${invalidTags.join(", ")}`,
});
}
} catch (error) {
return res.status(500).json({
error: "Error validating tags",
});
}
}
// Validate length parameters if both are provided
if (minLength !== null && maxLength !== null && minLength > maxLength) {
return res.status(400).json({
error: "minLength must be less than or equal to maxLength.",
});
}
const quotes = getQuotes({ maxLength, minLength, tags, count, authors });
if (quotes) {
res.json(count === 1 ? quotes[0] : quotes);
} else {
res.status(404).json({ error: "No quotes found matching the criteria." });
}
});
// Home page route
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
});
module.exports = app;