diff --git a/backend/adonisrc.ts b/backend/adonisrc.ts index cbf9c5cd..083b3db0 100644 --- a/backend/adonisrc.ts +++ b/backend/adonisrc.ts @@ -43,6 +43,7 @@ export default defineConfig({ environment: ["console"], }, () => import("@adonisjs/mail/mail_provider"), + () => import("@adonisjs/shield/shield_provider"), ], /* diff --git a/backend/config/cors.ts b/backend/config/cors.ts index ebcb3e71..2d120215 100644 --- a/backend/config/cors.ts +++ b/backend/config/cors.ts @@ -1,5 +1,7 @@ import { defineConfig } from "@adonisjs/cors"; +import env from "#start/env"; + /** * Configuration options to tweak the CORS policy. The following * options are documented on the official documentation website. @@ -8,7 +10,7 @@ import { defineConfig } from "@adonisjs/cors"; */ const corsConfig = defineConfig({ enabled: true, - origin: true, + origin: env.get("CORS_ORIGIN", "planer.solvro.pl").split(","), methods: ["GET", "HEAD", "POST", "PUT", "DELETE"], headers: true, exposeHeaders: [], diff --git a/backend/config/shield.ts b/backend/config/shield.ts new file mode 100644 index 00000000..621ea7aa --- /dev/null +++ b/backend/config/shield.ts @@ -0,0 +1,51 @@ +import { defineConfig } from "@adonisjs/shield"; + +const shieldConfig = defineConfig({ + /** + * Configure CSP policies for your app. Refer documentation + * to learn more + */ + csp: { + enabled: false, + directives: {}, + reportOnly: false, + }, + + /** + * Configure CSRF protection options. Refer documentation + * to learn more + */ + csrf: { + enabled: true, + exceptRoutes: ["/user/login"], + enableXsrfCookie: true, + methods: ["POST", "PUT", "PATCH", "DELETE"], + }, + + /** + * Control how your website should be embedded inside + * iFrames + */ + xFrame: { + enabled: true, + action: "DENY", + }, + + /** + * Force browser to always use HTTPS + */ + hsts: { + enabled: true, + maxAge: "180 days", + }, + + /** + * Disable browsers from sniffing the content type of a + * response and always rely on the "content-type" header. + */ + contentTypeSniffing: { + enabled: true, + }, +}); + +export default shieldConfig; diff --git a/backend/package-lock.json b/backend/package-lock.json index 7f0f983a..158406d8 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,6 +15,7 @@ "@adonisjs/lucid": "^21.2.0", "@adonisjs/mail": "^9.2.2", "@adonisjs/session": "^7.5.0", + "@adonisjs/shield": "^8.1.2", "@maximemrf/adonisjs-jwt": "^0.2.2", "@vinejs/vine": "^2.1.0", "adonis-autoswagger": "^3.64.0", @@ -604,6 +605,38 @@ } } }, + "node_modules/@adonisjs/shield": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@adonisjs/shield/-/shield-8.1.2.tgz", + "integrity": "sha512-ksC3KMTnGVYyjos/PM7WMQ/mUNhDuxvpFqNd2YAgPEWYVc+kr3WTTkaRrC7srl6jFmbOOv4R4aQ9RIvo1S0pjg==", + "license": "MIT", + "dependencies": { + "@poppinss/utils": "^6.9.2", + "csrf": "^3.1.0", + "helmet-csp": "^3.4.0" + }, + "engines": { + "node": ">=18.16.0" + }, + "peerDependencies": { + "@adonisjs/core": "^6.2.0", + "@adonisjs/i18n": "^2.0.0", + "@adonisjs/session": "^7.0.0", + "@japa/api-client": "^2.0.2 || ^3.0.0", + "edge.js": "^6.0.1" + }, + "peerDependenciesMeta": { + "@adonisjs/i18n": { + "optional": true + }, + "@japa/api-client": { + "optional": true + }, + "edge.js": { + "optional": true + } + } + }, "node_modules/@adonisjs/tsconfig": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@adonisjs/tsconfig/-/tsconfig-1.4.0.tgz", @@ -1642,6 +1675,15 @@ "node": ">=18.16.0" } }, + "node_modules/@poppinss/exception": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.0.tgz", + "integrity": "sha512-WLneXKQYNClhaMXccO111VQmZahSrcSRDaHRbV6KL5R4pTvK87fMn/MXLUcvOjk0X5dTHDPKF61tM7j826wrjQ==", + "license": "MIT", + "engines": { + "node": ">=20.6.0" + } + }, "node_modules/@poppinss/hooks": { "version": "7.2.4", "resolved": "https://registry.npmjs.org/@poppinss/hooks/-/hooks-7.2.4.tgz", @@ -1699,6 +1741,15 @@ "uid-safe": "2.1.5" } }, + "node_modules/@poppinss/object-builder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@poppinss/object-builder/-/object-builder-1.1.0.tgz", + "integrity": "sha512-FOrOq52l7u8goR5yncX14+k+Ewi5djnrt1JwXeS/FvnwAPOiveFhiczCDuvXdssAwamtrV2hp5Rw9v+n2T7hQg==", + "license": "MIT", + "engines": { + "node": ">=20.6.0" + } + }, "node_modules/@poppinss/prompts": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@poppinss/prompts/-/prompts-3.1.3.tgz", @@ -1713,29 +1764,58 @@ "node": ">=18.16.0" } }, - "node_modules/@poppinss/utils": { - "version": "6.8.3", - "resolved": "https://registry.npmjs.org/@poppinss/utils/-/utils-6.8.3.tgz", - "integrity": "sha512-YGeH7pIUm9ExONURNH3xN61dBZ0SXgVuPA9E76t7EHeZHXPNrmR8TlbXQaka6kd5n+cpBNcHG4VsVfYf59bZ7g==", + "node_modules/@poppinss/string": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@poppinss/string/-/string-1.2.0.tgz", + "integrity": "sha512-1z78zjqhfjqsvWr+pQzCpRNcZpIM+5vNY5SFOvz28GrL/LRanwtmOku5tBX7jE8/ng3oXaOVrB59lnnXFtvkug==", "license": "MIT", "dependencies": { "@lukeed/ms": "^2.0.2", - "@types/bytes": "^3.1.4", + "@types/bytes": "^3.1.5", "@types/pluralize": "^0.0.33", "bytes": "^3.1.2", "case-anything": "^3.1.0", - "flattie": "^1.1.1", "pluralize": "^8.0.0", - "safe-stable-stringify": "^2.5.0", - "secure-json-parse": "^2.7.0", - "slash": "^5.1.0", "slugify": "^1.6.6", "truncatise": "^0.0.8" }, + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@poppinss/utils": { + "version": "6.9.2", + "resolved": "https://registry.npmjs.org/@poppinss/utils/-/utils-6.9.2.tgz", + "integrity": "sha512-ypVszZxhwiehhklM5so2BI+nClQJwp7mBUSJh/R1GepeUH1vvD5GtxMz8Lp9dO9oAbKyDmq1jc4g/4E0dv8r2g==", + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.0", + "@poppinss/object-builder": "^1.1.0", + "@poppinss/string": "^1.1.0", + "flattie": "^1.1.1", + "safe-stable-stringify": "^2.5.0", + "secure-json-parse": "^3.0.1" + }, "engines": { "node": ">=18.16.0" } }, + "node_modules/@poppinss/utils/node_modules/secure-json-parse": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-3.0.2.tgz", + "integrity": "sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/@poppinss/validator-lite": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@poppinss/validator-lite/-/validator-lite-1.0.3.tgz", @@ -2249,9 +2329,9 @@ "license": "MIT" }, "node_modules/@types/bytes": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/bytes/-/bytes-3.1.4.tgz", - "integrity": "sha512-A0uYgOj3zNc4hNjHc5lYUfJQ/HVyBXiUMKdXd7ysclaE6k9oJdavQzODHuwjpUu2/boCP8afjQYi8z/GtvNCWA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/bytes/-/bytes-3.1.5.tgz", + "integrity": "sha512-VgZkrJckypj85YxEsEavcMmmSOIzkUHqWmM4CCyia5dc54YwsXzJ5uT4fYxBQNEXx+oF1krlhgCbvfubXqZYsQ==", "license": "MIT" }, "node_modules/@types/chai": { @@ -4236,6 +4316,20 @@ "node": "*" } }, + "node_modules/csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "license": "MIT", + "dependencies": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -6782,6 +6876,15 @@ "tslib": "^2.0.3" } }, + "node_modules/helmet-csp": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-3.4.0.tgz", + "integrity": "sha512-a+YgzWw6dajqhQfb6ktxil0FsQuWTKzrLSUfy55dxS8fuvl1jidTIMPZ2udN15mjjcpBPgTHNHGF5tyWKYyR8w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/help-me": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", @@ -10642,6 +10745,12 @@ "node": ">=0.10.0" } }, + "node_modules/rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10743,6 +10852,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/semver": { @@ -11782,6 +11892,15 @@ "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "license": "0BSD" }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/backend/package.json b/backend/package.json index d6ad11e3..97ba32bd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -46,6 +46,7 @@ "@adonisjs/lucid": "^21.2.0", "@adonisjs/mail": "^9.2.2", "@adonisjs/session": "^7.5.0", + "@adonisjs/shield": "^8.1.2", "@maximemrf/adonisjs-jwt": "^0.2.2", "@vinejs/vine": "^2.1.0", "adonis-autoswagger": "^3.64.0", diff --git a/backend/start/kernel.ts b/backend/start/kernel.ts index c2e261fe..d1c86cae 100644 --- a/backend/start/kernel.ts +++ b/backend/start/kernel.ts @@ -35,6 +35,7 @@ router.use([ () => import("@adonisjs/core/bodyparser_middleware"), () => import("@adonisjs/session/session_middleware"), () => import("@adonisjs/auth/initialize_auth_middleware"), + () => import("@adonisjs/shield/shield_middleware"), ]); /** diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6738bf65..a284b833 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -39,7 +39,7 @@ "jotai": "^2.9.3", "lru-cache": "^11.0.1", "lucide-react": "^0.426.0", - "next": "^15.1.4", + "next": "^15.1.7", "next-sitemap": "^4.2.3", "next-themes": "^0.4.3", "node-fetch": "^3.3.2", @@ -1108,9 +1108,9 @@ } }, "node_modules/@next/env": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.4.tgz", - "integrity": "sha512-2fZ5YZjedi5AGaeoaC0B20zGntEHRhi2SdWcu61i48BllODcAmmtj8n7YarSPt4DaTsJaBFdxQAVEVzgmx2Zpw==", + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.7.tgz", + "integrity": "sha512-d9jnRrkuOH7Mhi+LHav2XW91HOgTAWHxjMPkXMGBc9B2b7614P7kjt8tAplRvJpbSt4nbO1lugcT/kAaWzjlLQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1157,9 +1157,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.4.tgz", - "integrity": "sha512-wBEMBs+np+R5ozN1F8Y8d/Dycns2COhRnkxRc+rvnbXke5uZBHkUGFgWxfTXn5rx7OLijuUhyfB+gC/ap58dDw==", + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.7.tgz", + "integrity": "sha512-hPFwzPJDpA8FGj7IKV3Yf1web3oz2YsR8du4amKw8d+jAOHfYHYFpMkoF6vgSY4W6vB29RtZEklK9ayinGiCmQ==", "cpu": [ "arm64" ], @@ -1173,9 +1173,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.4.tgz", - "integrity": "sha512-7sgf5rM7Z81V9w48F02Zz6DgEJulavC0jadab4ZsJ+K2sxMNK0/BtF8J8J3CxnsJN3DGcIdC260wEKssKTukUw==", + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.7.tgz", + "integrity": "sha512-2qoas+fO3OQKkU0PBUfwTiw/EYpN+kdAx62cePRyY1LqKtP09Vp5UcUntfZYajop5fDFTjSxCHfZVRxzi+9FYQ==", "cpu": [ "x64" ], @@ -1189,9 +1189,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.4.tgz", - "integrity": "sha512-JaZlIMNaJenfd55kjaLWMfok+vWBlcRxqnRoZrhFQrhM1uAehP3R0+Aoe+bZOogqlZvAz53nY/k3ZyuKDtT2zQ==", + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.7.tgz", + "integrity": "sha512-sKLLwDX709mPdzxMnRIXLIT9zaX2w0GUlkLYQnKGoXeWUhcvpCrK+yevcwCJPdTdxZEUA0mOXGLdPsGkudGdnA==", "cpu": [ "arm64" ], @@ -1205,9 +1205,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.4.tgz", - "integrity": "sha512-7EBBjNoyTO2ipMDgCiORpwwOf5tIueFntKjcN3NK+GAQD7OzFJe84p7a2eQUeWdpzZvhVXuAtIen8QcH71ZCOQ==", + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.7.tgz", + "integrity": "sha512-zblK1OQbQWdC8fxdX4fpsHDw+VSpBPGEUX4PhSE9hkaWPrWoeIJn+baX53vbsbDRaDKd7bBNcXRovY1hEhFd7w==", "cpu": [ "arm64" ], @@ -1221,9 +1221,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.4.tgz", - "integrity": "sha512-9TGEgOycqZFuADyFqwmK/9g6S0FYZ3tphR4ebcmCwhL8Y12FW8pIBKJvSwV+UBjMkokstGNH+9F8F031JZKpHw==", + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.7.tgz", + "integrity": "sha512-GOzXutxuLvLHFDAPsMP2zDBMl1vfUHHpdNpFGhxu90jEzH6nNIgmtw/s1MDwpTOiM+MT5V8+I1hmVFeAUhkbgQ==", "cpu": [ "x64" ], @@ -1237,9 +1237,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.4.tgz", - "integrity": "sha512-0578bLRVDJOh+LdIoKvgNDz77+Bd85c5JrFgnlbI1SM3WmEQvsjxTA8ATu9Z9FCiIS/AliVAW2DV/BDwpXbtiQ==", + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.7.tgz", + "integrity": "sha512-WrZ7jBhR7ATW1z5iEQ0ZJfE2twCNSXbpCSaAunF3BKcVeHFADSI/AW1y5Xt3DzTqPF1FzQlwQTewqetAABhZRQ==", "cpu": [ "x64" ], @@ -1253,9 +1253,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.4.tgz", - "integrity": "sha512-JgFCiV4libQavwII+kncMCl30st0JVxpPOtzWcAI2jtum4HjYaclobKhj+JsRu5tFqMtA5CJIa0MvYyuu9xjjQ==", + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.7.tgz", + "integrity": "sha512-LDnj1f3OVbou1BqvvXVqouJZKcwq++mV2F+oFHptToZtScIEnhNRJAhJzqAtTE2dB31qDYL45xJwrc+bLeKM2Q==", "cpu": [ "arm64" ], @@ -1269,9 +1269,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.4.tgz", - "integrity": "sha512-xxsJy9wzq7FR5SqPCUqdgSXiNXrMuidgckBa8nH9HtjjxsilgcN6VgXF6tZ3uEWuVEadotQJI8/9EQ6guTC4Yw==", + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.7.tgz", + "integrity": "sha512-dC01f1quuf97viOfW05/K8XYv2iuBgAxJZl7mbCKEjMgdQl5JjAKJ0D2qMKZCgPWDeFbFT0Q0nYWwytEW0DWTQ==", "cpu": [ "x64" ], @@ -7709,12 +7709,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/next/-/next-15.1.4.tgz", - "integrity": "sha512-mTaq9dwaSuwwOrcu3ebjDYObekkxRnXpuVL21zotM8qE2W0HBOdVIdg2Li9QjMEZrj73LN96LcWcz62V19FjAg==", + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.1.7.tgz", + "integrity": "sha512-GNeINPGS9c6OZKCvKypbL8GTsT5GhWPp4DM0fzkXJuXMilOO2EeFxuAY6JZbtk6XIl6Ws10ag3xRINDjSO5+wg==", "license": "MIT", "dependencies": { - "@next/env": "15.1.4", + "@next/env": "15.1.7", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -7729,14 +7729,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.1.4", - "@next/swc-darwin-x64": "15.1.4", - "@next/swc-linux-arm64-gnu": "15.1.4", - "@next/swc-linux-arm64-musl": "15.1.4", - "@next/swc-linux-x64-gnu": "15.1.4", - "@next/swc-linux-x64-musl": "15.1.4", - "@next/swc-win32-arm64-msvc": "15.1.4", - "@next/swc-win32-x64-msvc": "15.1.4", + "@next/swc-darwin-arm64": "15.1.7", + "@next/swc-darwin-x64": "15.1.7", + "@next/swc-linux-arm64-gnu": "15.1.7", + "@next/swc-linux-arm64-musl": "15.1.7", + "@next/swc-linux-x64-gnu": "15.1.7", + "@next/swc-linux-x64-musl": "15.1.7", + "@next/swc-win32-arm64-msvc": "15.1.7", + "@next/swc-win32-x64-msvc": "15.1.7", "sharp": "^0.33.5" }, "peerDependencies": { diff --git a/frontend/package.json b/frontend/package.json index f961eefb..6d690467 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -51,7 +51,7 @@ "jotai": "^2.9.3", "lru-cache": "^11.0.1", "lucide-react": "^0.426.0", - "next": "^15.1.4", + "next": "^15.1.7", "next-sitemap": "^4.2.3", "next-themes": "^0.4.3", "node-fetch": "^3.3.2", diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt index 4f8e28f8..329d9536 100644 --- a/frontend/public/robots.txt +++ b/frontend/public/robots.txt @@ -2,6 +2,11 @@ User-agent: * Allow: / +# * +User-agent: * +Disallow: /plans +Disallow: /api + # Host Host: https://planer.solvro.pl diff --git a/frontend/src/actions/logout.ts b/frontend/src/actions/logout.ts index 84cddc9b..061d9e03 100644 --- a/frontend/src/actions/logout.ts +++ b/frontend/src/actions/logout.ts @@ -3,9 +3,15 @@ import { cookies as cookiesPromise } from "next/headers"; import { env } from "@/env.mjs"; +import { fetchToAdonis } from "@/lib/auth"; export const signOutFunction = async () => { const cookies = await cookiesPromise(); + await fetchToAdonis({ + url: `${env.NEXT_PUBLIC_API_URL}/user/logout`, + method: "DELETE", + }); + cookies.delete({ name: "access_token", path: "/", @@ -22,8 +28,10 @@ export const signOutFunction = async () => { name: "token", path: "/", }); - - await fetch(`${env.NEXT_PUBLIC_API_URL}/user/logout`, { method: "DELETE" }); + cookies.delete({ + name: "XSRF-TOKEN", + path: "/", + }); return true; }; diff --git a/frontend/src/app/api/callback/route.ts b/frontend/src/app/api/callback/route.ts index 28d2c12b..4802a2ab 100644 --- a/frontend/src/app/api/callback/route.ts +++ b/frontend/src/app/api/callback/route.ts @@ -74,12 +74,14 @@ export const GET = async (request: NextRequest) => { maxAge: 60 * 60 * 24 * 7, httpOnly: true, secure: true, + sameSite: "strict", }); cookies.set("access_token_secret", access_token.secret, { path: "/", maxAge: 60 * 60 * 24 * 7, httpOnly: true, secure: true, + sameSite: "strict", }); await auth(tokens); diff --git a/frontend/src/app/api/login/route.ts b/frontend/src/app/api/login/route.ts index e7ea7a41..b65efd45 100644 --- a/frontend/src/app/api/login/route.ts +++ b/frontend/src/app/api/login/route.ts @@ -29,6 +29,7 @@ export async function GET() { maxAge: 60 * 60 * 24 * 7, httpOnly: true, secure: true, + sameSite: "strict", }); cookies.set("oauth_token_secret", token.secret, { @@ -36,6 +37,7 @@ export async function GET() { maxAge: 60 * 60 * 24 * 7, httpOnly: true, secure: true, + sameSite: "strict", }); return redirect( diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index d16e502a..dac6cb38 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Space_Grotesk } from "next/font/google"; +import { headers } from "next/headers"; import Script from "next/script"; import type React from "react"; @@ -98,6 +99,8 @@ export default async function RootLayout({ children: React.ReactNode; }) { const user = await auth({ disableThrow: true }); + const headersList = await headers(); + const nonce = headersList.get("x-nonce"); return ( @@ -118,6 +121,7 @@ export default async function RootLayout({ src="https://analytics.solvro.pl/script.js" data-website-id="ab126a0c-c0ab-401b-bf9d-da652aab69ec" data-domains="planer.solvro.pl" + nonce={nonce ?? undefined} /> diff --git a/frontend/src/app/plans/_components/share-plan-button.tsx b/frontend/src/app/plans/_components/share-plan-button.tsx index 217f601c..f1171054 100644 --- a/frontend/src/app/plans/_components/share-plan-button.tsx +++ b/frontend/src/app/plans/_components/share-plan-button.tsx @@ -7,6 +7,7 @@ import { v4 as uuidv4 } from "uuid"; import { Icons } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { env } from "@/env.mjs"; +import { fetchClient } from "@/lib/fetch"; import type { PlanState } from "@/types"; export function SharePlanButton({ plan }: { plan: PlanState }) { @@ -34,11 +35,9 @@ export function SharePlanButton({ plan }: { plan: PlanState }) { const randomUUID = uuidv4(); try { - const response = await fetch(`${env.NEXT_PUBLIC_API_URL}/shared`, { + const response = await fetchClient({ + url: "/shared", method: "POST", - headers: { - "Content-Type": "application/json", - }, body: JSON.stringify({ plan: JSON.stringify(preparedData), id: randomUUID, diff --git a/frontend/src/app/plans/edit/[id]/page.client.tsx b/frontend/src/app/plans/edit/[id]/page.client.tsx index 3291d1a7..19c47dc9 100644 --- a/frontend/src/app/plans/edit/[id]/page.client.tsx +++ b/frontend/src/app/plans/edit/[id]/page.client.tsx @@ -36,9 +36,9 @@ import { SelectValue, } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; -import { env } from "@/env.mjs"; import { useSession } from "@/hooks/use-session"; import { useShare } from "@/hooks/use-share"; +import { fetchClient } from "@/lib/fetch"; import { usePlan } from "@/lib/use-plan"; import { registrationReplacer } from "@/lib/utils"; import { createOnlinePlan } from "@/lib/utils/create-online-plan"; @@ -79,9 +79,10 @@ export function CreateNewPlanPage({ enabled: faculty !== null && faculty !== "", queryKey: ["registrations", faculty], queryFn: async () => { - const response = await fetch( - `${env.NEXT_PUBLIC_API_URL}/departments/${encodeURIComponent(faculty ?? "")}/registrations`, - ); + const response = await fetchClient({ + url: `/departments/${encodeURIComponent(faculty ?? "")}/registrations`, + method: "GET", + }); if (!response.ok) { throw new Error("Network response was not ok"); @@ -116,9 +117,10 @@ export function CreateNewPlanPage({ const coursesFunction = useMutation({ mutationKey: ["courses"], mutationFn: async (registrationId: string) => { - const response = await fetch( - `${env.NEXT_PUBLIC_API_URL}/departments/${encodeURIComponent(faculty ?? "")}/registrations/${encodeURIComponent(registrationId)}/courses`, - ); + const response = await fetchClient({ + url: `/departments/${encodeURIComponent(faculty ?? "")}/registrations/${encodeURIComponent(registrationId)}/courses`, + method: "GET", + }); if (!response.ok) { throw new Error("Network response was not ok"); diff --git a/frontend/src/app/plans/edit/[id]/page.tsx b/frontend/src/app/plans/edit/[id]/page.tsx index d031ef84..d464ba0b 100644 --- a/frontend/src/app/plans/edit/[id]/page.tsx +++ b/frontend/src/app/plans/edit/[id]/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; +import { cookies as cookiesPromise } from "next/headers"; import { notFound } from "next/navigation"; import React from "react"; @@ -24,9 +25,18 @@ export default async function CreateNewPlan({ params }: PageProps) { if (typeof id !== "string" || id.length === 0) { return notFound(); } + const cookies = await cookiesPromise(); + const csrfToken = cookies.get("XSRF-TOKEN")?.value; const facultiesResponse = await fetch( `${env.NEXT_PUBLIC_API_URL}/departments`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-XSRF-TOKEN": csrfToken ?? "", + }, + }, ).then( async (r) => r.json() as Promise<{ id: string; name: string }[] | null>, ); diff --git a/frontend/src/lib/auth/index.ts b/frontend/src/lib/auth/index.ts index d6b2a442..4deb98a2 100644 --- a/frontend/src/lib/auth/index.ts +++ b/frontend/src/lib/auth/index.ts @@ -92,6 +92,8 @@ export async function getRequestToken() { }; } +const ADONIS_COOKIES = new Set(["token", "adonis-session"]); + export const auth = async (tokens?: { token?: string | undefined; secret?: string | undefined; @@ -139,14 +141,27 @@ export const auth = async (tokens?: { for (const cookie of setCookieHeaders) { const preparedCookie = cookie.split(";")[0]; const [name, value] = preparedCookie.split("="); - cookies.set({ - name, - value, - path: "/", - maxAge: 60 * 60 * 24 * 7, - httpOnly: true, - secure: true, - }); + if (name === "XSRF-TOKEN") { + cookies.set({ + name, + value, + path: "/", + maxAge: 60 * 60 * 24 * 7, + httpOnly: false, + secure: true, + sameSite: "strict", + }); + } else if (ADONIS_COOKIES.has(name)) { + cookies.set({ + name, + value, + path: "/", + maxAge: 60 * 60 * 24 * 7, + httpOnly: true, + secure: true, + sameSite: "strict", + }); + } } } catch {} @@ -178,6 +193,7 @@ export const fetchToAdonis = async ({ headers: { "Content-Type": "application/json", Cookie: `adonis-session=${adonisSession ?? ""}; token=${token ?? ""}`, + "X-XSRF-TOKEN": cookies.get("XSRF-TOKEN")?.value ?? "", }, credentials: "include", }; diff --git a/frontend/src/lib/fetch.ts b/frontend/src/lib/fetch.ts new file mode 100644 index 00000000..defe2946 --- /dev/null +++ b/frontend/src/lib/fetch.ts @@ -0,0 +1,34 @@ +"use client"; + +import { env } from "@/env.mjs"; + +export const fetchClient = async ({ + url, + method, + body, +}: { + url: string; + method: RequestInit["method"]; + body?: string | null; +}): Promise => { + const csrfToken = document.cookie + .split("; ") + .find((row) => row.startsWith("XSRF-TOKEN=")) + ?.split("=")[1]; + + const fetchOptions: RequestInit = { + method, + headers: { + "Content-Type": "application/json", + "X-XSRF-TOKEN": csrfToken ?? "", + }, + credentials: "include", + }; + + if (method !== "GET" && method !== "HEAD" && body !== undefined) { + fetchOptions.body = body; + } + + const response = fetch(`${env.NEXT_PUBLIC_API_URL}${url}`, fetchOptions); + return await response; +}; diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index ce264497..7e897c20 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -4,6 +4,42 @@ import { NextResponse } from "next/server"; import { auth } from "./lib/auth"; export async function middleware(request: NextRequest) { + const nonce = Buffer.from(crypto.randomUUID()).toString("base64"); + const cspHeader = ` + default-src 'self' 'nonce-${nonce}' https://fonts.googleapis.com https://fonts.gstatic.com ${process.env.NODE_ENV === "development" ? "'unsafe-eval'" : ""}; + script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${process.env.NODE_ENV === "development" ? "'unsafe-eval'" : ""} https://analytics.solvro.pl; + style-src 'self' 'nonce-${nonce}'; + img-src 'self' blob: data: https://avatars.githubusercontent.com https://wit.pwr.edu.pl https://cms.solvro.pl https://apps.usos.pwr.edu.pl; + font-src 'self'; + object-src 'none'; + base-uri 'self'; + form-action 'self'; + frame-ancestors 'none'; + upgrade-insecure-requests; + `; + + const contentSecurityPolicyHeaderValue = cspHeader + .replaceAll(/\s{2,}/g, " ") + .trim(); + + const requestHeaders = new Headers(request.headers); + requestHeaders.set("x-nonce", nonce); + + requestHeaders.set( + "Content-Security-Policy", + contentSecurityPolicyHeaderValue, + ); + + const nextResponse = NextResponse.next({ + request: { + headers: requestHeaders, + }, + }); + nextResponse.headers.set( + "Content-Security-Policy", + contentSecurityPolicyHeaderValue, + ); + const tokens = { token: request.cookies.get("access_token")?.value, secret: request.cookies.get("access_token_secret")?.value, @@ -14,12 +50,31 @@ export async function middleware(request: NextRequest) { const user = await auth(tokens); if (!isProtectedRoute) { - return NextResponse.next(); + return nextResponse; } if (user === null) { return NextResponse.redirect(new URL("/", request.url)); } - return NextResponse.next(); + return nextResponse; } + +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - api (API routes) + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + */ + { + source: "/((?!api|_next/static|_next/image|favicon.ico).*)", + missing: [ + { type: "header", key: "next-router-prefetch" }, + { type: "header", key: "purpose", value: "prefetch" }, + ], + }, + ], +};