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" },
+ ],
+ },
+ ],
+};