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 # base64-encoded APNs private key
APNS_PRIVATE_KEY= APNS_PRIVATE_KEY=
# control parking data logging
PARKING_LOGGING_INTERVAL_MS= PARKING_LOGGING_INTERVAL_MS=
PARKING_HISTORICAL_AVERAGE_MINIMUM_INTERVAL= PARKING_HISTORICAL_AVERAGE_MINIMUM_INTERVAL=
PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN= 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} APNS_PRIVATE_KEY: ${APNS_PRIVATE_KEY}
PARKING_LOGGING_INTERVAL_MS: ${PARKING_LOGGING_INTERVAL_MS} PARKING_LOGGING_INTERVAL_MS: ${PARKING_LOGGING_INTERVAL_MS}
REDIS_URL: redis://redis:6379 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: services:
dev: dev:
@@ -33,6 +37,7 @@ services:
- redis-no-persistence - redis-no-persistence
environment: environment:
REDIS_URL: redis://redis-no-persistence:6379 REDIS_URL: redis://redis-no-persistence:6379
RATE_LIMITS_DISABLED: 1
volumes: volumes:
- .:/usr/src/app - .:/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": "4.1.2",
"@graphql-codegen/typescript-resolvers": "4.4.1", "@graphql-codegen/typescript-resolvers": "4.4.1",
"@jest/globals": "^29.7.0", "@jest/globals": "^29.7.0",
"@types/express": "^5.0.3",
"@types/jsonwebtoken": "^9.0.8", "@types/jsonwebtoken": "^9.0.8",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/redis": "^4.0.11", "@types/redis": "^4.0.11",
@@ -26,6 +27,10 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@apollo/server": "^4.11.2", "@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", "graphql": "^16.10.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"redis": "^4.7.0" "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_MINIMUM_INTERVAL = 1000;
export const PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN = 60000 * 60 * 24; 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 export const PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN = process.env.PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN
? parseInt(process.env.PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN) ? parseInt(process.env.PARKING_HISTORICAL_AVERAGE_MAXIMUM_TIMESPAN)
: 60000 * 60 * 24; : 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 { readFileSync } from "fs";
import { ApolloServer } from "@apollo/server"; import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import { MergedResolvers } from "./MergedResolvers"; import { MergedResolvers } from "./MergedResolvers";
import { ServerContext } from "./ServerContext"; import { ServerContext } from "./ServerContext";
import { loadShuttleTestData } from "./loaders/shuttle/loadShuttleTestData";
import { InterchangeSystem, InterchangeSystemBuilderArguments } from "./entities/InterchangeSystem"; import { InterchangeSystem, InterchangeSystemBuilderArguments } from "./entities/InterchangeSystem";
import { ChapmanApiBasedParkingRepositoryLoader } from "./loaders/parking/ChapmanApiBasedParkingRepositoryLoader"; import { ChapmanApiBasedParkingRepositoryLoader } from "./loaders/parking/ChapmanApiBasedParkingRepositoryLoader";
import { supportedIntegrationTestSystems } from "./loaders/supportedIntegrationTestSystems"; import express from "express";
import { loadParkingTestData } from "./loaders/parking/loadParkingTestData"; 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"); const typeDefs = readFileSync("./schema.graphqls", "utf8");
@@ -27,37 +32,18 @@ async function main() {
resolvers: MergedResolvers, resolvers: MergedResolvers,
introspection: process.env.NODE_ENV !== "production", introspection: process.env.NODE_ENV !== "production",
}); });
await server.start();
let systems: InterchangeSystem[]; let systems: InterchangeSystem[];
if (process.argv.length > 2 && process.argv[2] == "integration-testing") { systems = await Promise.all(supportedSystems.map(
console.log("Using integration testing setup") async (systemArguments) => {
return await InterchangeSystem.build(systemArguments);
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,
}, },
));
const app = express();
const options = {
context: async () => { context: async () => {
return { return {
systems, 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(); main();