diff --git a/.env.example b/.env.example index 6520381..bda00c7 100644 --- a/.env.example +++ b/.env.example @@ -34,5 +34,12 @@ CLUB_NAME = clubname #default FF Admin CLUB_WEBSITE = https://my-club-website-url #optional, muss aber mit http:// oder https:// beginnen BACKUP_INTERVAL = number of days (min 1) # default 1 -BACKUP_COPIES = number of parallel copies #default 7 -BACKUP_AUTO_RESTORE = (true|false) # default false \ No newline at end of file +BACKUP_COPIES = number of parallel copies # default 7 +BACKUP_AUTO_RESTORE = (true|false) # default ist true + +USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true +SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15 +SECURITY_STRICT_LIMIT_REQUEST_COUNT = strict_request_count # default ist 15 +USE_SECURITY_LIMIT = (true|false) # default ist true +SECURITY_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 1m +SECURITY_LIMIT_REQUEST_COUNT = request_count # default ist 500 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d112b4b..88943f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,15 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^5.0.0-beta.3", + "express-rate-limit": "^7.5.0", + "express-validator": "^7.2.1", "handlebars": "^4.7.8", + "helmet": "^8.0.0", "ics": "^3.8.1", "jsonwebtoken": "^9.0.2", "lodash.uniqby": "^4.7.0", "moment": "^2.30.1", + "morgan": "^1.10.0", "ms": "^2.1.3", "multer": "^1.4.5-lts.1", "mysql": "^2.18.1", @@ -39,6 +43,7 @@ "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.6", "@types/lodash.uniqby": "^4.7.9", + "@types/morgan": "^1.9.9", "@types/ms": "^0.7.34", "@types/multer": "^1.4.12", "@types/mysql": "^2.15.21", @@ -563,6 +568,16 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/morgan": { + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", + "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -956,6 +971,24 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -2045,6 +2078,34 @@ "node": ">= 4" } }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/express-validator": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", + "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.12.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/express/node_modules/debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", @@ -2405,6 +2466,15 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.0.0.tgz", + "integrity": "sha512-VyusHLEIIO5mjQPUI1wpOAEu+wl6Q0998jzTxqUYGE45xCIcAxy3MsbEK/yyJUJ3ADeMoB6MornPH6GMWAf+Pw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -2772,6 +2842,12 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -3248,6 +3324,34 @@ "node": "*" } }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3531,6 +3635,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5482,6 +5595,15 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "devOptional": true }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 81892af..9e1a00c 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,15 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^5.0.0-beta.3", + "express-rate-limit": "^7.5.0", + "express-validator": "^7.2.1", "handlebars": "^4.7.8", + "helmet": "^8.0.0", "ics": "^3.8.1", "jsonwebtoken": "^9.0.2", "lodash.uniqby": "^4.7.0", "moment": "^2.30.1", + "morgan": "^1.10.0", "ms": "^2.1.3", "multer": "^1.4.5-lts.1", "mysql": "^2.18.1", @@ -54,6 +58,7 @@ "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.6", "@types/lodash.uniqby": "^4.7.9", + "@types/morgan": "^1.9.9", "@types/ms": "^0.7.34", "@types/multer": "^1.4.12", "@types/mysql": "^2.15.21", diff --git a/src/env.defaults.ts b/src/env.defaults.ts index 19024c2..4c8362d 100644 --- a/src/env.defaults.ts +++ b/src/env.defaults.ts @@ -28,6 +28,13 @@ export const BACKUP_INTERVAL = Number(process.env.BACKUP_INTERVAL ?? "1"); export const BACKUP_COPIES = Number(process.env.BACKUP_COPIES ?? "7"); export const BACKUP_AUTO_RESTORE = process.env.BACKUP_AUTO_RESTORE ?? "true"; +export const USE_SECURITY_STRICT_LIMIT = process.env.USE_SECURITY_STRICT_LIMIT ?? "true"; +export const SECURITY_STRICT_LIMIT_WINDOW = process.env.SECURITY_STRICT_LIMIT_WINDOW ?? "15m"; +export const SECURITY_STRICT_LIMIT_REQUEST_COUNT = Number(process.env.SECURITY_STRICT_LIMIT_REQUEST_COUNT ?? "15"); +export const USE_SECURITY_LIMIT = process.env.USE_SECURITY_LIMIT ?? "true"; +export const SECURITY_LIMIT_WINDOW = process.env.SECURITY_LIMIT_WINDOW ?? "1m"; +export const SECURITY_LIMIT_REQUEST_COUNT = Number(process.env.SECURITY_LIMIT_REQUEST_COUNT ?? "500"); + export function configCheck() { if (DB_TYPE != "mysql" && DB_TYPE != "sqlite" && DB_TYPE != "postgres") throw new Error("set valid value to DB_TYPE (mysql|sqlite|postgres)"); @@ -62,15 +69,26 @@ export function configCheck() { throw new Error("set 'true' or 'false' to BACKUP_AUTO_RESTORE"); if (BACKUP_INTERVAL < 1) throw new Error("BACKUP_INTERVAL has to be at least 1"); if (BACKUP_COPIES < 1) throw new Error("BACKUP_COPIES has to be at least 1"); + + if (USE_SECURITY_STRICT_LIMIT != "true" && USE_SECURITY_STRICT_LIMIT != "false") + throw new Error("set 'true' or 'false' to USE_SECURITY_STRICT_LIMIT"); + checkMS(SECURITY_STRICT_LIMIT_WINDOW, "SECURITY_STRICT_LIMIT_WINDOW"); + if (typeof SECURITY_STRICT_LIMIT_REQUEST_COUNT != "number") + throw new Error("set valid numeric value to SECURITY_STRICT_LIMIT_REQUEST_COUNT"); + if (USE_SECURITY_LIMIT != "true" && USE_SECURITY_LIMIT != "false") + throw new Error("set 'true' or 'false' to USE_SECURITY_LIMIT"); + checkMS(SECURITY_LIMIT_WINDOW, "SECURITY_LIMIT_WINDOW"); + if (typeof SECURITY_LIMIT_REQUEST_COUNT != "number") + throw new Error("set valid numeric value to SECURITY_LIMIT_REQUEST_COUNT"); } function checkMS(input: string, origin: string) { try { const result = ms(input); if (result === undefined) { - throw new Error(`set valid ms value to ${origin}`); + throw new Error(`set valid ms value to ${origin} -> [0-9]*(y|d|h|m|s)`); } } catch (e) { - throw new Error(`set valid ms value to ${origin}`); + throw new Error(`set valid ms value to ${origin} -> [0-9]*(y|d|h|m|s)`); } } diff --git a/src/factory/admin/club/member/dateMappingHelper.ts b/src/factory/admin/club/member/dateMappingHelper.ts new file mode 100644 index 0000000..ab20076 --- /dev/null +++ b/src/factory/admin/club/member/dateMappingHelper.ts @@ -0,0 +1,14 @@ +import { DB_TYPE } from "../../../../env.defaults"; + +export default abstract class DateMappingHelper { + static mapDate(entry: any) { + switch (DB_TYPE) { + case "postgres": + return `${entry.years} years ${entry.months} months ${entry.days} days`; + case "mysql": + return entry.toString(); + case "sqlite": + return entry; + } + } +} diff --git a/src/factory/admin/club/member/member.ts b/src/factory/admin/club/member/member.ts index 95a46cb..536b012 100644 --- a/src/factory/admin/club/member/member.ts +++ b/src/factory/admin/club/member/member.ts @@ -3,6 +3,7 @@ import { MemberStatisticsViewModel, MemberViewModel } from "../../../../viewmode import { memberView } from "../../../../views/memberView"; import SalutationFactory from "../../settings/salutation"; import CommunicationFactory from "./communication"; +import DateMappingHelper from "./dateMappingHelper"; import MembershipFactory from "./membership"; export default abstract class MemberFactory { @@ -59,7 +60,7 @@ export default abstract class MemberFactory { birthdate: record.birthdate, todayAge: record.todayAge, ageThisYear: record.ageThisYear, - exactAge: record.exactAge, + exactAge: DateMappingHelper.mapDate(record.exactAge), }; } } diff --git a/src/factory/admin/club/member/membership.ts b/src/factory/admin/club/member/membership.ts index 72089ae..b71d56c 100644 --- a/src/factory/admin/club/member/membership.ts +++ b/src/factory/admin/club/member/membership.ts @@ -4,6 +4,7 @@ import { MembershipViewModel, } from "../../../../viewmodel/admin/club/member/membership.models"; import { membershipView } from "../../../../views/membershipsView"; +import DateMappingHelper from "./dateMappingHelper"; export default abstract class MembershipFactory { /** @@ -40,7 +41,7 @@ export default abstract class MembershipFactory { return { durationInDays: record.durationInDays, durationInYears: record.durationInYears, - exactDuration: record.exactDuration.toString(), + exactDuration: DateMappingHelper.mapDate(record.exactDuration), status: record.status, statusId: record.statusId, memberId: record.memberId, diff --git a/src/middleware/allowSetup.ts b/src/middleware/allowSetup.ts index 18844bd..981d955 100644 --- a/src/middleware/allowSetup.ts +++ b/src/middleware/allowSetup.ts @@ -1,8 +1,8 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import UserService from "../service/user/userService"; import CustomRequestException from "../exceptions/customRequestException"; -export default async function allowSetup(req: Request, res: Response, next: Function) { +export default async function allowSetup(req: Request, res: Response, next: NextFunction) { let count = await UserService.count(); if (count != 0) { throw new CustomRequestException(405, "service is already set up"); diff --git a/src/middleware/authenticate.ts b/src/middleware/authenticate.ts index 9e62bb4..abeb832 100644 --- a/src/middleware/authenticate.ts +++ b/src/middleware/authenticate.ts @@ -1,11 +1,11 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import jwt from "jsonwebtoken"; import BadRequestException from "../exceptions/badRequestException"; import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException"; import InternalException from "../exceptions/internalException"; import { JWTHelper } from "../helpers/jwtHelper"; -export default async function authenticate(req: Request, res: Response, next: Function) { +export default async function authenticate(req: Request, res: Response, next: NextFunction) { const bearer = req.headers.authorization?.split(" ")?.[1] ?? undefined; if (!bearer) { diff --git a/src/middleware/authenticateAPI.ts b/src/middleware/authenticateAPI.ts index b05060e..7b87474 100644 --- a/src/middleware/authenticateAPI.ts +++ b/src/middleware/authenticateAPI.ts @@ -1,11 +1,11 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import jwt from "jsonwebtoken"; import BadRequestException from "../exceptions/badRequestException"; import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException"; import InternalException from "../exceptions/internalException"; import { JWTHelper } from "../helpers/jwtHelper"; -export default async function authenticateAPI(req: Request, res: Response, next: Function) { +export default async function authenticateAPI(req: Request, res: Response, next: NextFunction) { const bearer = req.headers.authorization?.split(" ")?.[1] ?? undefined; if (!bearer) { diff --git a/src/middleware/detectPWA.ts b/src/middleware/detectPWA.ts index d30df95..0aa3b0f 100644 --- a/src/middleware/detectPWA.ts +++ b/src/middleware/detectPWA.ts @@ -1,6 +1,6 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; -export default async function detectPWA(req: Request, res: Response, next: Function) { +export default async function detectPWA(req: Request, res: Response, next: NextFunction) { const userAgent = req.headers["user-agent"] || ""; if ((userAgent.includes("Mobile") && userAgent.includes("Standalone")) || req.headers["x-pwa-client"] === "true") { req.isPWA = true; diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts index 92e45ad..0ffe741 100644 --- a/src/middleware/errorHandler.ts +++ b/src/middleware/errorHandler.ts @@ -1,9 +1,9 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import { ExceptionBase } from "../exceptions/exceptionsBaseType"; import CustomRequestException from "../exceptions/customRequestException"; import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException"; -export default function errorHandler(err: ExceptionBase | Error, req: Request, res: Response, next: Function) { +export default function errorHandler(err: ExceptionBase | Error, req: Request, res: Response, next: NextFunction) { let status = 500; let msg = "Internal Server Error"; diff --git a/src/middleware/preventWebApiAccess.ts b/src/middleware/preventWebApiAccess.ts index 3c3b7c6..534c2fd 100644 --- a/src/middleware/preventWebApiAccess.ts +++ b/src/middleware/preventWebApiAccess.ts @@ -1,7 +1,7 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import ForbiddenRequestException from "../exceptions/forbiddenRequestException"; -export default async function preventWebapiAccess(req: Request, res: Response, next: Function) { +export default async function preventWebapiAccess(req: Request, res: Response, next: NextFunction) { if (req.isWebApiRequest) { throw new ForbiddenRequestException("This route cannot be accessed via webapi"); } else { diff --git a/src/migrations/baseSchemaTables/member.ts b/src/migrations/baseSchemaTables/member.ts index 422f3cd..c5f304b 100644 --- a/src/migrations/baseSchemaTables/member.ts +++ b/src/migrations/baseSchemaTables/member.ts @@ -336,7 +336,7 @@ export const member_executive_positions_view_postgres = new View({ "member"."birthdate" AS "memberBirthdate", "salutation"."salutation" AS "memberSalutation", SUM(COALESCE("memberExecutivePositions"."end", CURRENT_DATE) - "memberExecutivePositions"."start") AS "durationInDays", - SUM(EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM member.birthdate)) AS "durationInYears", + SUM(EXTRACT(YEAR FROM AGE(COALESCE("memberExecutivePositions"."end", CURRENT_DATE), "memberExecutivePositions"."start"))) AS "durationInYears", SUM(AGE(COALESCE("memberExecutivePositions"."end", CURRENT_DATE), "memberExecutivePositions"."start")) AS "exactDuration" FROM "member_executive_positions" "memberExecutivePositions" LEFT JOIN "executive_position" "executivePosition" ON "executivePosition"."id"="memberExecutivePositions"."executivePositionId" @@ -415,7 +415,8 @@ export const member_qualifications_view_postgres = new View({ "member"."birthdate" AS "memberBirthdate", "salutation"."salutation" AS "memberSalutation", SUM(COALESCE("memberQualifications"."end", CURRENT_DATE) - "memberQualifications"."start") AS "durationInDays", - SUM(AGE(COALESCE("memberQualifications"."end", CURRENT_DATE), "memberQualifications"."start")) AS "durationInYears" + SUM(EXTRACT(YEAR FROM AGE(COALESCE("memberQualifications"."end", CURRENT_DATE), "memberQualifications"."start"))) AS "durationInYears", + SUM(AGE(COALESCE("memberQualifications"."end", CURRENT_DATE), "memberQualifications"."start")) AS "exactDuration" FROM "member_qualifications" "memberQualifications" LEFT JOIN "qualification" "qualification" ON "qualification"."id"="memberQualifications"."qualificationId" LEFT JOIN "member" "member" ON "member"."id"="memberQualifications"."memberId" @@ -493,7 +494,8 @@ export const membership_view_postgres = new View({ "member"."birthdate" AS "memberBirthdate", "salutation"."salutation" AS "memberSalutation", SUM(COALESCE("membership"."end", CURRENT_DATE) - "membership"."start") AS "durationInDays", - SUM(AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start")) AS "durationInYears" + SUM(EXTRACT(YEAR FROM AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start"))) AS "durationInYears", + SUM(AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start")) AS "exactDuration" FROM "membership" "membership" LEFT JOIN "membership_status" "status" ON "status"."id"="membership"."statusId" LEFT JOIN "member" "member" ON "member"."id"="membership"."memberId" diff --git a/src/routes/index.ts b/src/routes/index.ts index eca2062..ed95543 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,6 +1,9 @@ import express from "express"; -import type { Express } from "express"; +import type { Express, NextFunction, Request, RequestHandler, Response } from "express"; import cors from "cors"; +import helmet from "helmet"; +import morgan from "morgan"; +import rateLimit from "express-rate-limit"; import allowSetup from "../middleware/allowSetup"; import authenticate from "../middleware/authenticate"; @@ -19,26 +22,67 @@ import authenticateAPI from "../middleware/authenticateAPI"; import server from "./server"; import PermissionHelper from "../helpers/permissionHelper"; import preventWebapiAccess from "../middleware/preventWebApiAccess"; +import ms from "ms"; +import { + SECURITY_LIMIT_REQUEST_COUNT, + SECURITY_LIMIT_WINDOW, + SECURITY_STRICT_LIMIT_REQUEST_COUNT, + SECURITY_STRICT_LIMIT_WINDOW, + USE_SECURITY_LIMIT, + USE_SECURITY_STRICT_LIMIT, +} from "../env.defaults"; + +const strictLimiter = rateLimit({ + windowMs: ms(SECURITY_STRICT_LIMIT_WINDOW), + max: SECURITY_STRICT_LIMIT_REQUEST_COUNT, + message: `Zu viele Anmeldeversuche innerhalb von ${SECURITY_STRICT_LIMIT_WINDOW}. Bitte warten.`, + skipSuccessfulRequests: true, + skip: () => { + return USE_SECURITY_STRICT_LIMIT == "false"; + }, +}); + +const generalLimiter = rateLimit({ + windowMs: ms(SECURITY_LIMIT_WINDOW), + max: SECURITY_LIMIT_REQUEST_COUNT, + message: `Zu viele Anfragen innerhalb von ${SECURITY_LIMIT_WINDOW}. Bitte warten.`, + skipSuccessfulRequests: true, + skip: () => { + return USE_SECURITY_LIMIT == "false"; + }, +}); + +function excludePaths(middleware: RequestHandler, excludedPaths: Array) { + return (req: Request, res: Response, next: NextFunction) => { + if (excludedPaths.includes(req.path)) { + return next(); + } + return middleware(req, res, next); + }; +} export default (app: Express) => { app.set("query parser", "extended"); + app.use(cors()); + app.options("*", cors()); + app.use(helmet()); + app.use(morgan("short")); app.use(express.json()); app.use( express.urlencoded({ extended: true, }) ); - app.use(cors()); - app.options("*", cors()); app.use(detectPWA); app.use("/api/public", publicAvailable); - app.use("/api/setup", preventWebapiAccess, allowSetup, setup); - app.use("/api/reset", preventWebapiAccess, reset); - app.use("/api/invite", preventWebapiAccess, invite); - app.use("/api/auth", preventWebapiAccess, auth); + app.use("/api/setup", strictLimiter, preventWebapiAccess, allowSetup, setup); + app.use("/api/reset", strictLimiter, preventWebapiAccess, reset); + app.use("/api/invite", strictLimiter, preventWebapiAccess, invite); + app.use("/api/auth", strictLimiter, preventWebapiAccess, auth); app.use("/api/webapi", authenticateAPI, webapi); app.use(authenticate); + app.use(excludePaths(generalLimiter, ["/synchronize"])); app.use("/api/admin", admin); app.use("/api/user", preventWebapiAccess, user); app.use("/api/server", preventWebapiAccess, PermissionHelper.isAdminMiddleware(), server); diff --git a/src/views/memberExecutivePositionView.ts b/src/views/memberExecutivePositionView.ts index f82e35c..8b8f328 100644 --- a/src/views/memberExecutivePositionView.ts +++ b/src/views/memberExecutivePositionView.ts @@ -7,7 +7,7 @@ let durationInYears: string; let exactDuration: string; if (DB_TYPE == "postgres") { durationInDays = `SUM(COALESCE("memberExecutivePositions"."end", CURRENT_DATE) - "memberExecutivePositions"."start")`; - durationInYears = `SUM(EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM member.birthdate))`; + durationInYears = `SUM(EXTRACT(YEAR FROM AGE(COALESCE("memberExecutivePositions"."end", CURRENT_DATE), "memberExecutivePositions"."start")))`; exactDuration = `SUM(AGE(COALESCE("memberExecutivePositions"."end", CURRENT_DATE), "memberExecutivePositions"."start"))`; } else if (DB_TYPE == "mysql") { durationInDays = `SUM(DATEDIFF(COALESCE(memberExecutivePositions.end, CURDATE()), memberExecutivePositions.start))`; diff --git a/src/views/memberQualificationsView.ts b/src/views/memberQualificationsView.ts index 45d6089..6f88f39 100644 --- a/src/views/memberQualificationsView.ts +++ b/src/views/memberQualificationsView.ts @@ -7,7 +7,7 @@ let durationInYears: string; let exactDuration: string; if (DB_TYPE == "postgres") { durationInDays = `SUM(COALESCE("memberQualifications"."end", CURRENT_DATE) - "memberQualifications"."start") `; - durationInYears = `SUM(EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM member.birthdate))`; + durationInYears = `SUM(EXTRACT(YEAR FROM AGE(COALESCE("memberQualifications"."end", CURRENT_DATE), "memberQualifications"."start")))`; exactDuration = `SUM(AGE(COALESCE("memberQualifications"."end", CURRENT_DATE), "memberQualifications"."start"))`; } else if (DB_TYPE == "mysql") { durationInDays = `SUM(DATEDIFF(COALESCE(memberQualifications.end, CURDATE()), memberQualifications.start))`; diff --git a/src/views/membershipsView.ts b/src/views/membershipsView.ts index 68d1927..fc38c23 100644 --- a/src/views/membershipsView.ts +++ b/src/views/membershipsView.ts @@ -7,7 +7,7 @@ let durationInYears: string; let exactDuration: string; if (DB_TYPE == "postgres") { durationInDays = `SUM(COALESCE("membership"."end", CURRENT_DATE) - "membership"."start") `; - durationInYears = `SUM(EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM member.birthdate))`; + durationInYears = `SUM(EXTRACT(YEAR FROM AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start")))`; exactDuration = `SUM(AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start"))`; } else if (DB_TYPE == "mysql") { durationInDays = `SUM(DATEDIFF(COALESCE(membership.end, CURDATE()), membership.start))`;