Merge pull request #68 from brendan-ch/chore/rate-limits

chore/rate-limits
This commit is contained in:
2025-08-26 17:30:31 -07:00
committed by GitHub
7 changed files with 662 additions and 251 deletions

View File

@@ -8,6 +8,13 @@ APNS_BUNDLE_ID=
# base64-encoded APNs private key
APNS_PRIVATE_KEY=
# control parking data logging
PARKING_LOGGING_INTERVAL_MS=
PARKING_HISTORICAL_AVERAGE_MINIMUM_INTERVAL=
PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN=
# control rate limiting features
RATE_LIMITS_DISABLED=
RATE_LIMIT_WINDOW_MS=
RATE_LIMIT_DELAY_AFTER_REQUESTS=
RATE_LIMIT_DELAY_MULTIPLIER_MS=

View File

@@ -12,6 +12,10 @@ x-common-environment: &common-server-environment
APNS_PRIVATE_KEY: ${APNS_PRIVATE_KEY}
PARKING_LOGGING_INTERVAL_MS: ${PARKING_LOGGING_INTERVAL_MS}
REDIS_URL: redis://redis:6379
RATE_LIMITS_DISABLED: ${RATE_LIMITS_DISABLED}
RATE_LIMIT_WINDOW_MS: ${RATE_LIMIT_WINDOW_MS}
RATE_LIMIT_DELAY_AFTER_REQUESTS: ${RATE_LIMIT_DELAY_AFTER_REQUESTS}
RATE_LIMIT_DELAY_MULTIPLIER_MS: ${RATE_LIMIT_DELAY_MULTIPLIER_MS}
services:
dev:
@@ -33,6 +37,7 @@ services:
- redis-no-persistence
environment:
REDIS_URL: redis://redis-no-persistence:6379
RATE_LIMITS_DISABLED: 1
volumes:
- .:/usr/src/app

800
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@
"@graphql-codegen/typescript": "4.1.2",
"@graphql-codegen/typescript-resolvers": "4.4.1",
"@jest/globals": "^29.7.0",
"@types/express": "^5.0.3",
"@types/jsonwebtoken": "^9.0.8",
"@types/node": "^22.10.2",
"@types/redis": "^4.0.11",
@@ -26,6 +27,10 @@
"private": true,
"dependencies": {
"@apollo/server": "^4.11.2",
"@as-integrations/express5": "^1.1.2",
"express": "^5.1.0",
"express-rate-limit": "^8.0.1",
"express-slow-down": "^3.0.0",
"graphql": "^16.10.0",
"jsonwebtoken": "^9.0.2",
"redis": "^4.7.0"

View File

@@ -3,3 +3,9 @@ export const PARKING_LOGGING_INTERVAL_MS = 10000;
export const PARKING_HISTORICAL_AVERAGE_MINIMUM_INTERVAL = 1000;
export const PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN = 60000 * 60 * 24;
export const RATE_LIMITS_DISABLED = true;
export const RATE_LIMIT_WINDOW_MS = 10000;
export const RATE_LIMIT_DELAY_AFTER_REQUESTS = 10000;
export const RATE_LIMIT_DELAY_MULTIPLIER_MS = 1000;

View File

@@ -9,3 +9,15 @@ export const PARKING_HISTORICAL_AVERAGE_MINIMUM_INTERVAL = process.env.PARKING_H
export const PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN = process.env.PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN
? parseInt(process.env.PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN)
: 60000 * 60 * 24;
export const RATE_LIMITS_DISABLED = process.env.RATE_LIMITS_DISABLED === "1";
export const RATE_LIMIT_WINDOW_MS = process.env.RATE_LIMIT_WINDOW_MS
? parseInt(process.env.RATE_LIMIT_WINDOW_MS)
: 10000;
export const RATE_LIMIT_DELAY_AFTER_REQUESTS = process.env.RATE_LIMIT_DELAY_AFTER_REQUESTS
? parseInt(process.env.RATE_LIMIT_DELAY_AFTER_REQUESTS)
: 10000;
export const RATE_LIMIT_DELAY_MULTIPLIER_MS = process.env.RATE_LIMIT_DELAY_MULTIPLIER_MS
? parseInt(process.env.RATE_LIMIT_DELAY_MULTIPLIER_MS)
: 1000;

View File

@@ -1,13 +1,18 @@
import { readFileSync } from "fs";
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import { MergedResolvers } from "./MergedResolvers";
import { ServerContext } from "./ServerContext";
import { loadShuttleTestData } from "./loaders/shuttle/loadShuttleTestData";
import { InterchangeSystem, InterchangeSystemBuilderArguments } from "./entities/InterchangeSystem";
import { ChapmanApiBasedParkingRepositoryLoader } from "./loaders/parking/ChapmanApiBasedParkingRepositoryLoader";
import { supportedIntegrationTestSystems } from "./loaders/supportedIntegrationTestSystems";
import { loadParkingTestData } from "./loaders/parking/loadParkingTestData";
import express from "express";
import { expressMiddleware } from "@as-integrations/express5";
import {
RATE_LIMIT_DELAY_AFTER_REQUESTS,
RATE_LIMIT_DELAY_MULTIPLIER_MS,
RATE_LIMIT_WINDOW_MS,
RATE_LIMITS_DISABLED
} from "./environment";
import slowDown from "express-slow-down";
const typeDefs = readFileSync("./schema.graphqls", "utf8");
@@ -27,37 +32,18 @@ async function main() {
resolvers: MergedResolvers,
introspection: process.env.NODE_ENV !== "production",
});
await server.start();
let systems: InterchangeSystem[];
if (process.argv.length > 2 && process.argv[2] == "integration-testing") {
console.log("Using integration testing setup")
systems = await Promise.all(supportedIntegrationTestSystems.map(
async (systemArguments) => {
const system = InterchangeSystem.buildForTesting(systemArguments);
// TODO: Have loading of different data for different systems in the future
await loadShuttleTestData(system.shuttleRepository);
if (system.parkingRepository) {
await loadParkingTestData(system.parkingRepository);
}
return system;
}
));
} else {
systems = await Promise.all(supportedSystems.map(
async (systemArguments) => {
return await InterchangeSystem.build(systemArguments);
},
));
}
const { url } = await startStandaloneServer(server, {
listen: {
port: process.env.PORT ? parseInt(process.env.PORT) : 4000,
systems = await Promise.all(supportedSystems.map(
async (systemArguments) => {
return await InterchangeSystem.build(systemArguments);
},
));
const app = express();
const options = {
context: async () => {
return {
systems,
@@ -70,10 +56,34 @@ async function main() {
},
}
},
};
if (RATE_LIMITS_DISABLED) {
app.use(
"/",
express.json(),
expressMiddleware(server, options)
);
} else {
const limiter = slowDown({
windowMs: RATE_LIMIT_WINDOW_MS,
delayAfter: RATE_LIMIT_DELAY_AFTER_REQUESTS,
delayMs: (hits) => {
return hits * RATE_LIMIT_DELAY_MULTIPLIER_MS;
}
});
app.use(
"/",
express.json(),
limiter,
expressMiddleware(server, options),
);
}
const port = process.env.PORT ? parseInt(process.env.PORT) : 4000;
app.listen(port, () => {
console.log(`Server ready at port ${port}`);
});
console.log(`Server ready at: ${url}`);
}
main();