From 97c1a287bee5ea96c1eee867f6d1e4219cfce30f Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Wed, 27 May 2026 10:57:57 +0200 Subject: [PATCH 01/48] Sandra testar --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d6aeeb20..a46f9565 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# yh-message-app-fullstack \ No newline at end of file +# yh-message-app-fullstack + +Sandra testar. \ No newline at end of file From e42f394f219570b40e20649cd9cca5bec155453f Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Wed, 27 May 2026 10:59:59 +0200 Subject: [PATCH 02/48] Sandra test 2 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a46f9565..fcc34617 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # yh-message-app-fullstack -Sandra testar. \ No newline at end of file +Sandra testar. Och testar igen, tjohoo. \ No newline at end of file From 50910b0149b8077c742cd51c2fe532a16eca1562 Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Wed, 27 May 2026 11:26:17 +0200 Subject: [PATCH 03/48] =?UTF-8?q?Inl=C3=A4mning=20av=20planering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- planeringsfasen.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/planeringsfasen.md b/planeringsfasen.md index 661cae0e..cd3705ac 100644 --- a/planeringsfasen.md +++ b/planeringsfasen.md @@ -1 +1,3 @@ -# Inlämning 1 - Planeringsfasen \ No newline at end of file +# Inlämning 1 - Planeringsfasen + +https://www.figma.com/board/UitaJT4H8hl28KNGnIGbRr/Projekt-%22s%C3%A4ker-systemutveckling%22?node-id=0-1&p=f&t=lcQEilGf5fm5rGqb-0 \ No newline at end of file From beb423b216a80c428c36ab9d23204a1edfee60f2 Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Wed, 27 May 2026 11:28:58 +0200 Subject: [PATCH 04/48] Add screenshot to planeringsfasen.md Added an image screenshot to the planning phase document. --- planeringsfasen.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/planeringsfasen.md b/planeringsfasen.md index cd3705ac..a85645ce 100644 --- a/planeringsfasen.md +++ b/planeringsfasen.md @@ -1,3 +1,5 @@ # Inlämning 1 - Planeringsfasen -https://www.figma.com/board/UitaJT4H8hl28KNGnIGbRr/Projekt-%22s%C3%A4ker-systemutveckling%22?node-id=0-1&p=f&t=lcQEilGf5fm5rGqb-0 \ No newline at end of file +https://www.figma.com/board/UitaJT4H8hl28KNGnIGbRr/Projekt-%22s%C3%A4ker-systemutveckling%22?node-id=0-1&p=f&t=lcQEilGf5fm5rGqb-0 + +Screenshot 2026-05-27 112741 From 2ce1381d4c171c6ce19a4610b06578513f87702a Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Wed, 27 May 2026 11:32:11 +0200 Subject: [PATCH 05/48] =?UTF-8?q?Tog=20bort=20on=C3=B6dig=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- planeringsfasen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/planeringsfasen.md b/planeringsfasen.md index a85645ce..777cb916 100644 --- a/planeringsfasen.md +++ b/planeringsfasen.md @@ -2,4 +2,4 @@ https://www.figma.com/board/UitaJT4H8hl28KNGnIGbRr/Projekt-%22s%C3%A4ker-systemutveckling%22?node-id=0-1&p=f&t=lcQEilGf5fm5rGqb-0 -Screenshot 2026-05-27 112741 +src="https://github.com/user-attachments/assets/81752987-8af4-44f9-941d-00d2dd029f6d" /> From b07af4146d2e1179f5c898c2a1ca0261f57018a6 Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Wed, 27 May 2026 11:34:09 +0200 Subject: [PATCH 06/48] testtest --- planeringsfasen.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/planeringsfasen.md b/planeringsfasen.md index cd3705ac..ca92f355 100644 --- a/planeringsfasen.md +++ b/planeringsfasen.md @@ -1,3 +1,4 @@ # Inlämning 1 - Planeringsfasen -https://www.figma.com/board/UitaJT4H8hl28KNGnIGbRr/Projekt-%22s%C3%A4ker-systemutveckling%22?node-id=0-1&p=f&t=lcQEilGf5fm5rGqb-0 \ No newline at end of file +https://www.figma.com/board/UitaJT4H8hl28KNGnIGbRr/Projekt-%22s%C3%A4ker-systemutveckling%22?node-id=0-1&p=f&t=lcQEilGf5fm5rGqb-0 + From cbbe2acf252e0313c065cad5634c16afdf9efae5 Mon Sep 17 00:00:00 2001 From: christinevaren1984-crypto Date: Wed, 27 May 2026 16:00:45 +0200 Subject: [PATCH 07/48] Update README with additional test message --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fcc34617..ab058443 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # yh-message-app-fullstack -Sandra testar. Och testar igen, tjohoo. \ No newline at end of file +Sandra testar. Och testar igen, tjohoo. +Christine lägger in en rad.. testing testing From c712b1502bcdf953eba3c0d708309bd5d2bdfa62 Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Fri, 29 May 2026 10:28:42 +0200 Subject: [PATCH 08/48] Anteckningar --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index ab058443..7653b66d 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,36 @@ Sandra testar. Och testar igen, tjohoo. Christine lägger in en rad.. testing testing + +# yh-message-app-fullstack + +Sandra testar. Och testar igen, tjohoo. +Christine lägger in en rad.. testing testing + +DUBBELKOLLA +Inloggningsformulär tillåter enbart text och har maxantal tecken - DUBBELKOLLA KODEN VIA AI FÖR ATT SE ATT DETTA FINNS PÅ PLATS + +Sanitering av all inskickad text via exempelvis ett ORM, Object-Relational Mapper. - DUBBELKOLLA KODEN VIA AI FÖR ATT SE ATT DETTA FINNS PÅ PLATS + +- Anv ska ej kunna radera/ändra andras mess ?? +- Meddelanden ska ej kunna hanteras utan inlogg ?? DUBBELKOLLA KODEN VIA AI FÖR ATT SE OM DETTA FINNS + +Salting + Hashing - KOLLA HUR MAN GÖR OCH VAR? + +Samtliga händelser loggas, när och varifrån - KOLLA MED AI HUR OCH VAR +--------------------------------- +KODNING/AKTIVERING +- Vi ändrar i .env-filen - JWT_SECRET +- Minimikrav för lösenord, 10 tecken, 1 siffra, 1 specialtecken - SERVER.JS - FINNS I JS +- Implementera Session Timeout, 60 min - SERVER.JS - FINNS I JS +- Begränsa antal försök att logga in (5 försök, sedan paus på 15 min) - SERVER.JS +- Max 3 antal inloggade enheter samtidigt - godkännande via annan enhet vid ny inloggning - SERVER.JS +- Revision av dependencies - NPM AUDIT - kommando - i package.json - Github Dependent Bot (funktion) +- ta bort undantaget för env.example i .gitignore +------------------------------------ +BOCKA AV: +- Användaren ska skapa meddelanden +- Anv ska inloggat kunna ta bort och redigera egna mess +------------------------------------ +REKOMMENDATIONER: +- Rutiner för patching, revision av dependencies och back-uper. From 08ebd26eb3bcd53355a2543b0762a9702e78e9c8 Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Fri, 29 May 2026 10:29:42 +0200 Subject: [PATCH 09/48] Anteckningar --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 7653b66d..e5a36105 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,6 @@ Sandra testar. Och testar igen, tjohoo. Christine lägger in en rad.. testing testing -# yh-message-app-fullstack - -Sandra testar. Och testar igen, tjohoo. -Christine lägger in en rad.. testing testing DUBBELKOLLA Inloggningsformulär tillåter enbart text och har maxantal tecken - DUBBELKOLLA KODEN VIA AI FÖR ATT SE ATT DETTA FINNS PÅ PLATS From 2c6c636fb17177493480cd34f05ca7d0d24e2083 Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Fri, 29 May 2026 10:30:09 +0200 Subject: [PATCH 10/48] Anteckningar --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e5a36105..71059032 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ Sandra testar. Och testar igen, tjohoo. Christine lägger in en rad.. testing testing - DUBBELKOLLA Inloggningsformulär tillåter enbart text och har maxantal tecken - DUBBELKOLLA KODEN VIA AI FÖR ATT SE ATT DETTA FINNS PÅ PLATS @@ -31,3 +30,4 @@ BOCKA AV: ------------------------------------ REKOMMENDATIONER: - Rutiner för patching, revision av dependencies och back-uper. + From 8fae300c1e44308ec0ab7a5dff6d1668493734ff Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Mon, 1 Jun 2026 14:31:47 +0200 Subject: [PATCH 11/48] =?UTF-8?q?Sm=C3=A5=C3=A4ndringar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e5a36105..20d2f95d 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Samtliga händelser loggas, när och varifrån - KOLLA MED AI HUR OCH VAR KODNING/AKTIVERING - Vi ändrar i .env-filen - JWT_SECRET - Minimikrav för lösenord, 10 tecken, 1 siffra, 1 specialtecken - SERVER.JS - FINNS I JS -- Implementera Session Timeout, 60 min - SERVER.JS - FINNS I JS +- Implementera Session Timeout, 60 min - SERVER.JS - FINNS I JS (rad 28 + 76) - Begränsa antal försök att logga in (5 försök, sedan paus på 15 min) - SERVER.JS - Max 3 antal inloggade enheter samtidigt - godkännande via annan enhet vid ny inloggning - SERVER.JS - Revision av dependencies - NPM AUDIT - kommando - i package.json - Github Dependent Bot (funktion) From 30f61ada1d668b0ee132de0086b51b05f2bf9b33 Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Mon, 1 Jun 2026 14:32:05 +0200 Subject: [PATCH 12/48] =?UTF-8?q?Sm=C3=A5=C3=A4ndringar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 20d2f95d..fadfc535 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,39 @@ Salting + Hashing - KOLLA HUR MAN GÖR OCH VAR? Samtliga händelser loggas, när och varifrån - KOLLA MED AI HUR OCH VAR --------------------------------- KODNING/AKTIVERING -- Vi ändrar i .env-filen - JWT_SECRET +- Vi ändrar i .env-filen - JWT_SECRET - NEJ!!!! - Minimikrav för lösenord, 10 tecken, 1 siffra, 1 specialtecken - SERVER.JS - FINNS I JS - Implementera Session Timeout, 60 min - SERVER.JS - FINNS I JS (rad 28 + 76) -- Begränsa antal försök att logga in (5 försök, sedan paus på 15 min) - SERVER.JS +- Begränsa antal försök att logga in (5 försök, sedan paus på 15 min) - SERVER.JS (finns ej) - Max 3 antal inloggade enheter samtidigt - godkännande via annan enhet vid ny inloggning - SERVER.JS - Revision av dependencies - NPM AUDIT - kommando - i package.json - Github Dependent Bot (funktion) - ta bort undantaget för env.example i .gitignore + +-------------------------------------------- + +RAD 168 - SAKNAR AUTHENTICATEUSER - ÄNDRA TILL +import jwt from "jsonwebtoken" +import { User } from "../models/User.js" + +export const authenticateUser = async (req, res, next) => { + const token = req.headers.authorization?.replace("Bearer ", "") + if (!token) { + return res.status(401).json({ success: false, message: "No token provided" }) + } + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET) + const user = await User.findById(decoded.userId) + if (!user) { + return res.status(401).json({ success: false, message: "User not found" }) + } + req.user = user + next() + } catch (err) { + res.status(401).json({ success: false, message: "Invalid token" }) + } +} + + ------------------------------------ BOCKA AV: - Användaren ska skapa meddelanden From a7f672d9da4bfb7bbe226c413dc1beea987f399b Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Mon, 1 Jun 2026 15:23:44 +0200 Subject: [PATCH 13/48] Express Validator installation + revision --- backend/models/User.js | 2 +- backend/package-lock.json | 29 +++++++++++++++++++++++++++++ backend/package.json | 1 + backend/server.js | 34 ++++++++++++++++++++-------------- frontend/package-lock.json | 29 +++++++++++++++++++++++++++++ frontend/package.json | 3 ++- 6 files changed, 82 insertions(+), 16 deletions(-) diff --git a/backend/models/User.js b/backend/models/User.js index 266a60b4..bb456d4c 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -5,7 +5,7 @@ const userSchema = new mongoose.Schema({ type: String, required: true, unique: true, - minlength: 2, + minlength: 6, // Minimum username length of 6 characters trim: true, }, email: { diff --git a/backend/package-lock.json b/backend/package-lock.json index d8c4b3cd..7b0311f7 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,6 +13,7 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "express-list-endpoints": "^6.0.0", + "express-validator": "^7.3.2", "helmet": "^7.1.0", "jsonwebtoken": "8.5.1", "mongoose": "^8.4.0", @@ -619,6 +620,19 @@ "node": ">=10" } }, + "node_modules/express-validator": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.2.tgz", + "integrity": "sha512-ctLw1Vl6dXVH62dIQMDdTAQkrh480mkFuG6/SGXOaVlwPNukhRAe7EgJIMJ2TSAni8iwHBRp530zAZE5ZPF2IA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.18.1", + "validator": "~13.15.23" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1108,6 +1122,12 @@ "node": ">=12.0.0" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2156,6 +2176,15 @@ "node": ">= 0.4.0" } }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "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/backend/package.json b/backend/package.json index 1f47ffb6..35bc310a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "express-list-endpoints": "^6.0.0", + "express-validator": "^7.3.2", "helmet": "^7.1.0", "jsonwebtoken": "8.5.1", "mongoose": "^8.4.0", diff --git a/backend/server.js b/backend/server.js index c8d0c218..6a23e703 100644 --- a/backend/server.js +++ b/backend/server.js @@ -10,6 +10,7 @@ import { User } from "./models/User.js" import { authenticateUser } from "./middleware/auth.js" import "./config/db.js" import listEndpoints from "express-list-endpoints" +import { body, validationResult } from "express-validator" // Importerade express-validator för ökad validering av indata if (!process.env.JWT_SECRET) throw new Error("JWT_SECRET is not set in .env") @@ -25,34 +26,39 @@ app.get("/", (req, res) => { res.send(listEndpoints(app)) }) -app.post("/register", async (req, res) => { +app.post("/register", [ + body("username").isLength({ min: 6, max: 25 }).withMessage("Username must be between 6 and 25 characters"), // Adderade validering för användarnamn + body("email").isEmail().normalizeEmail(), // Validerar att email är i korrekt format och normaliserar det + body("password").isLength({ min: 10, max: 64 }).withMessage("Password must be between 10 and 64 characters") // Adderade validering för lösenordslängd +], async (req, res) => { + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(400).json({ success: false, message: "Failed to register. Check your user name and password.", errors: errors.array() }) // Förenklade felmeddelandet och inkluderade detaljerad information om valideringsfel i svaret. + } + try { const { email, password, username } = req.body - if (!username || username.trim().length < 2) { - return res.status(400).json({ success: false, message: "Username must be at least 2 characters" }) - } - const existingUser = await User.findOne({ - $or: [{ email: email.toLowerCase() }, { username: username.trim() }] + $or: [{ email: email }, { username: username.trim() }] }) if (existingUser) { - const field = existingUser.email === email.toLowerCase() ? "email" : "username" + const field = existingUser.email === email ? "email" : "username" return res.status(400).json({ success: false, message: `A user with this ${field} already exists` }) } - const hashedPassword = await bcrypt.hash(password, 10) - const user = new User({ username: username.trim(), email, password: hashedPassword }) + const hashedPassword = await bcrypt.hash(password, 10) + const user = new User({ username: username.trim(), email, password: hashedPassword }) await user.save() const accessToken = jwt.sign( { userId: user._id, username: user.username }, process.env.JWT_SECRET, - { expiresIn: "2h" } + { expiresIn: "1h" } ) res.status(201).json({ @@ -64,14 +70,14 @@ app.post("/register", async (req, res) => { accessToken, }, }) - } catch (error) { + } catch (error) { // <--- FIX 3: Fångar upp eventuella fel under processen res.status(400).json({ success: false, message: "Could not create user", error: error, }) - } -}) + } +}) app.post("/login", async (req, res) => { try { @@ -100,7 +106,7 @@ app.post("/login", async (req, res) => { const accessToken = jwt.sign( { userId: user._id, username: user.username }, process.env.JWT_SECRET, - { expiresIn: "2h" } + { expiresIn: "1h" } //Lowered token expiration time to 1 hour for better security ) res.json({ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e43d824e..aa9ce4b7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "date-fns": "^2.30.0", + "express-validator": "^7.3.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -2065,6 +2066,19 @@ "node": ">=0.10.0" } }, + "node_modules/express-validator": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.2.tgz", + "integrity": "sha512-ctLw1Vl6dXVH62dIQMDdTAQkrh480mkFuG6/SGXOaVlwPNukhRAe7EgJIMJ2TSAni8iwHBRp530zAZE5ZPF2IA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.18.1", + "validator": "~13.15.23" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3103,6 +3117,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4305,6 +4325,15 @@ "punycode": "^2.1.0" } }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vite": { "version": "4.5.14", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", diff --git a/frontend/package.json b/frontend/package.json index fc417bb3..670fd598 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "date-fns": "^2.30.0", + "express-validator": "^7.3.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -24,4 +25,4 @@ "eslint-plugin-react-refresh": "^0.4.3", "vite": "^4.4.5" } -} \ No newline at end of file +} From 9c77292f00dda2f74015785f277fc65099e884f9 Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Mon, 1 Jun 2026 15:42:22 +0200 Subject: [PATCH 14/48] Save 2 --- backend/server.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/backend/server.js b/backend/server.js index 6a23e703..d36cc260 100644 --- a/backend/server.js +++ b/backend/server.js @@ -58,7 +58,7 @@ app.post("/register", [ const accessToken = jwt.sign( { userId: user._id, username: user.username }, process.env.JWT_SECRET, - { expiresIn: "1h" } + { expiresIn: "1h" } // Sänkte token expiration time till 1 timme för ökad säkerhet ) res.status(201).json({ @@ -70,7 +70,7 @@ app.post("/register", [ accessToken, }, }) - } catch (error) { // <--- FIX 3: Fångar upp eventuella fel under processen + } catch (error) { res.status(400).json({ success: false, message: "Could not create user", @@ -79,7 +79,16 @@ app.post("/register", [ } }) -app.post("/login", async (req, res) => { +app.post("/login", [ // Validering av indata för login adderad via Express-validator + body("login").trim().notEmpty().withMessage("Username or email is required"), + body("password").notEmpty().withMessage("Password is required") +], async (req, res) => { + // 1. Kontrollera om valideringen hittade tomma fält + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(400).json({ success: false, message: "Validation failed", errors: errors.array() }) + } + try { const { login, password } = req.body const user = await User.findOne({ @@ -106,7 +115,7 @@ app.post("/login", async (req, res) => { const accessToken = jwt.sign( { userId: user._id, username: user.username }, process.env.JWT_SECRET, - { expiresIn: "1h" } //Lowered token expiration time to 1 hour for better security + { expiresIn: "1h" } //Sänkte token expiration time till 1 timme för ökad säkerhet ) res.json({ @@ -129,6 +138,12 @@ app.post("/login", async (req, res) => { const isValidId = (id) => mongoose.Types.ObjectId.isValid(id) + + + + + + app.get("/messages", async (req, res) => { try { const messages = await Message.find() From b94a9803a4e45f8bcedacc17f9acc65c8b81b605 Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Mon, 1 Jun 2026 16:16:44 +0200 Subject: [PATCH 15/48] Inputverifiering --- backend/server.js | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/backend/server.js b/backend/server.js index d36cc260..94001ffb 100644 --- a/backend/server.js +++ b/backend/server.js @@ -144,7 +144,7 @@ const isValidId = (id) => mongoose.Types.ObjectId.isValid(id) -app.get("/messages", async (req, res) => { +app.get("/messages", authenticateUser, async (req, res) => { //Lade till authenticateUser för att säkerställa att endast inloggade användare kan hämta meddelanden. try { const messages = await Message.find() .sort({ createdAt: "desc" }) @@ -157,9 +157,17 @@ app.get("/messages", async (req, res) => { } }) -app.post("/messages", authenticateUser, async (req, res) => { - const message = new Message({ message: req.body.message, user: req.user._id }) +app.post("/messages", [ + authenticateUser, + body("message").trim().isLength({ min: 1, max: 500 }).withMessage("Message must be between 1 and 500 characters") +], async (req, res) => { + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(400).json({ success: false, errors: errors.array() }) + } + try { + const message = new Message({ message: req.body.message, user: req.user._id }) const saved = await message.save() res.status(201).json(saved) } catch (err) { @@ -167,8 +175,18 @@ app.post("/messages", authenticateUser, async (req, res) => { } }) -app.patch("/messages/:id", authenticateUser, async (req, res) => { +app.patch("/messages/:id", [ + authenticateUser, + body("editedMessage").trim().isLength({ min: 1, max: 250 }).withMessage("Edited message must be between 1 and 250 characters") //Adderade validering för det redigerade meddelandet, så att det inte kan vara tomt eller för långt. +], async (req, res) => { if (!isValidId(req.params.id)) return res.status(400).json({ error: "Invalid message ID" }) + + + const errors = validationResult(req) // Lade const errors efter ID-kontrollen, så att vi inte gör onödiga databasförfrågningar om ID:t redan är ogiltigt. Det gör också att vi kan returnera mer specifika felmeddelanden till klienten, antingen om ID:t är ogiltigt eller om det finns problem med det redigerade meddelandet. + if (!errors.isEmpty()) { + return res.status(400).json({ success: false, errors: errors.array() }) + } + try { const message = await Message.findById(req.params.id) if (!message) return res.status(404).json({ error: "Message not found" }) @@ -186,11 +204,16 @@ app.patch("/messages/:id", authenticateUser, async (req, res) => { } }) -app.delete("/messages/:id", async (req, res) => { +app.delete("/messages/:id", authenticateUser, async (req, res) => { //Lade till authenticateUser för att säkerställa att endast inloggade användare kan radera meddelanden, och att de endast kan radera sina egna meddelanden. if (!isValidId(req.params.id)) return res.status(400).json({ error: "Invalid message ID" }) try { const message = await Message.findById(req.params.id) if (!message) return res.status(404).json({ error: "Message not found" }) + + if (message.user.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "You can only delete your own messages" }) + } + await message.deleteOne() res.status(204).send() } catch (error) { From 870fa859151396d3da781eb83e551d6873569cc3 Mon Sep 17 00:00:00 2001 From: linnhass Date: Tue, 2 Jun 2026 11:22:43 +0200 Subject: [PATCH 16/48] Adderat isOwner --- frontend/src/components/SingleMessage.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/SingleMessage.jsx b/frontend/src/components/SingleMessage.jsx index 50d2274b..e381718e 100644 --- a/frontend/src/components/SingleMessage.jsx +++ b/frontend/src/components/SingleMessage.jsx @@ -83,7 +83,9 @@ export const SingleMessage = ({ message, user, onUnauthorized, fetchPosts }) => )}
- + {isOwner && ( //endast visa delete-knappen för ägaren av meddelandet + + )} {isOwner && !isEditing && ( From f2a0f27b9845796e55fd1395ee531668889fbea8 Mon Sep 17 00:00:00 2001 From: linnhass Date: Tue, 2 Jun 2026 11:46:34 +0200 Subject: [PATCH 17/48] =?UTF-8?q?f=C3=B6reslagit=20=C3=A4ndring=20av=20ses?= =?UTF-8?q?ion-tokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/server.js b/backend/server.js index 94001ffb..49558b4f 100644 --- a/backend/server.js +++ b/backend/server.js @@ -21,6 +21,7 @@ app.use(cors({ origin: "*", })) app.use(express.json()) +// Vi föreslår att säkra session-tokeln genom att använda HttpOnly cookies istället för att skicka den i JSON-responsen, vilket minskar risken för XSS-attacker. Det här innebär större ändringar i övrig kod för att hantera autentisering och session, vi har valt att inte genomföra detta själva. app.get("/", (req, res) => { res.send(listEndpoints(app)) From e46b13b1bae6917c5adde37ee75a7cca68ccfb8a Mon Sep 17 00:00:00 2001 From: linnhass Date: Tue, 2 Jun 2026 12:17:22 +0200 Subject: [PATCH 18/48] =?UTF-8?q?kommentar=20om=20rekomendation=20om=20att?= =?UTF-8?q?=20g=C3=B6ra=20meddelanden=20osynliga=20f=C3=B6r=20ej=20inlogga?= =?UTF-8?q?de=20anv=C3=A4ndare?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 0 -> 8196 bytes frontend/src/App.jsx | 2 +- package-lock.json | 6 ++++++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .DS_Store create mode 100644 package-lock.json diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fa31f3057f03b1c5eeeff45406a52b30ca2fb253 GIT binary patch literal 8196 zcmeHM&u<$=6n>KivL>|M{Gw?D(yG27H3Wkqq6(oJHxWb#C5odop)F?DJ8`$(%vihY zgf@!gGo0YgAArP(6A~AWT)1*W{0VU71mFB{YwQH19;#AiteNla%=_N%y!G3+&n6;P z>&9;o%@dJ_uF#*va7t6_s*kiWJ97cZz$Z$mO(7LPIil?*oCll-oCll-oCll-o(2yf zXRGF&k@wB28=VK72mY5HP|t?|U18N?&!B#EV9+H1a2(URK}H>*If2Eh#hyV;g-ugE zFiT~ )Sw^+2fus}_3(4V{>w6SHS#wnAb0?BE4jomkbN8=VK72Sz=hYWD@y>>Yg4 zzt!)8a*Bv!Y*U7nLlPJTwP=YBC|*xxGfHIyz6!Y-`qH1eK_Z#}H>4)G9Gqe8Z~s|4 zI?6lN_IFVyP4c2r`I(KK8-MQk32(xi^zPK}%A zRj!?!l)Sm}>u+y0+UbFipXpr^vyUS>Z;l_1pF%AdUnbp!Rztzf;xZVkyiD>N>{L`YI zJm3v~dPsGP#~(kM+P>d`&g`QJZSK8{bD`~R?CIZ${RU`ORHUi#VXZw;xR{ntpTHY2?@KUK<-Ly*{U8Q&FL)yWY z@&SE8U(wg}J^e_(&~Nkyn_!pO6?T;^vv=5a_C8x@AF(F8%My0a@W=YM`W0)>0?Q!` z-Ri@7CQ>O{SWzsG_7Or7fl!c#ce`+9=T$t>IC%iig$O znO+!Vz`$d)bA~go>i@N~fB%0B%Z}na;5_hO@cT>Gl&(mC;mf#dnJA{uf#6@tE%~_&HsM^YQe%$ literal 0 HcmV?d00001 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c668778f..0d76fd67 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -18,7 +18,7 @@ export const App = () => { .then((data) => setMessageList(data)) .catch((error) => console.error(error)) .finally(() => setLoading(false)) - } + }//vi rekommenderar att man gör meddelanden osynliga för ej inloggade användare. Åtgärdat i backend men inte i frontend, vi har valt att inte implementera detta i frontend-koden för att inte göra den onödigt komplex. useEffect(() => { fetchPosts() }, []) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..2c710ff5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "yh-message-app-fullstack", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From d7db263ee8169c86ae6d8e01bfd4b1598e63d139 Mon Sep 17 00:00:00 2001 From: haxxmonkey-ship-it Date: Wed, 3 Jun 2026 10:13:33 +0200 Subject: [PATCH 19/48] ENV Create --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 872d5f6c..30d878c4 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,7 @@ dist vite.config.js.timestamp-* vite.config.ts.timestamp-* .vite/ + +# Environment variables file +.env + From 76617890e7bfad98f1e93816b093bcd22583613a Mon Sep 17 00:00:00 2001 From: haxxmonkey-ship-it Date: Wed, 3 Jun 2026 11:20:21 +0200 Subject: [PATCH 20/48] Adjusted logged off message view --- frontend/src/App.jsx | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0d76fd67..25d7070f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,16 +12,39 @@ export const App = () => { const [error, setError] = useState(null) const fetchPosts = () => { + // Om det inte finns någon inloggad användare eller token, avbryt hämtningen direkt + if (!user?.response?.accessToken) { + setLoading(false) + return + } + setLoading(true) - fetch(`${BASE_URL}/messages`) - .then((res) => res.json()) - .then((data) => setMessageList(data)) + fetch(`${BASE_URL}/messages`, { + headers: { + // Skickar med användarens JWT-token till backend + Authorization: `Bearer ${user.response.accessToken}`, + }, + }) + .then((res) => { + if (res.status === 401) { + handleUnauthorized() + return [] + } + return res.json() + }) + .then((data) => { + if (Array.isArray(data)) { + setMessageList(data) + } + }) .catch((error) => console.error(error)) .finally(() => setLoading(false)) - }//vi rekommenderar att man gör meddelanden osynliga för ej inloggade användare. Åtgärdat i backend men inte i frontend, vi har valt att inte implementera detta i frontend-koden för att inte göra den onödigt komplex. + } + useEffect(() => { + // Triggar en hämtning så fort komponenten laddas eller när 'user' ändras (t.ex. vid inloggning) fetchPosts() - }, []) + }, [user]) const addNewPost = (newMessage) => { setMessageList([newMessage, ...messageList]) From 89f7f9c5183370ceb584c319322e176ad336dfb0 Mon Sep 17 00:00:00 2001 From: haxxmonkey-ship-it Date: Wed, 3 Jun 2026 11:20:39 +0200 Subject: [PATCH 21/48] Frontend adjustment --- frontend/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 670fd598..2a5ed32f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,6 @@ }, "dependencies": { "date-fns": "^2.30.0", - "express-validator": "^7.3.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, From 3054b46823b6b287be54042a63c0b57806f0a7bd Mon Sep 17 00:00:00 2001 From: linnhass Date: Wed, 3 Jun 2026 15:28:48 +0200 Subject: [PATCH 22/48] =?UTF-8?q?Adderat=20loginLimiter=20f=C3=B6r=20att?= =?UTF-8?q?=20begr=C3=A4nsa=20antal=20f=C3=B6rs=C3=B6kt=20till=20inloggnin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/middleware/loginLiminter.js | 10 + backend/server.js | 3 +- frontend/src/App.jsx | 2 +- package-lock.json | 929 +++++++++++++++++++++++++++- package.json | 5 + 5 files changed, 946 insertions(+), 3 deletions(-) create mode 100644 backend/middleware/loginLiminter.js create mode 100644 package.json diff --git a/backend/middleware/loginLiminter.js b/backend/middleware/loginLiminter.js new file mode 100644 index 00000000..c50524d2 --- /dev/null +++ b/backend/middleware/loginLiminter.js @@ -0,0 +1,10 @@ +import rateLimit from "express-rate-limit"; + +export const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minuter + max: 5, + message: { + success: false, + message: "För många inloggningsförsök. Försök igen om 15 minuter." + } +}); \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 49558b4f..7b167af3 100644 --- a/backend/server.js +++ b/backend/server.js @@ -11,6 +11,7 @@ import { authenticateUser } from "./middleware/auth.js" import "./config/db.js" import listEndpoints from "express-list-endpoints" import { body, validationResult } from "express-validator" // Importerade express-validator för ökad validering av indata +import { loginLimiter } from "./middleware/loginLimiter.js" // importerar loginLimiter middleware för att begränsa inloggningsförsök och skydda mot brute-force attacker if (!process.env.JWT_SECRET) throw new Error("JWT_SECRET is not set in .env") @@ -80,7 +81,7 @@ app.post("/register", [ } }) -app.post("/login", [ // Validering av indata för login adderad via Express-validator +app.post("/login", loginLimiter, [ // Validering av indata för login adderad via Express-validator body("login").trim().notEmpty().withMessage("Username or email is required"), body("password").notEmpty().withMessage("Password is required") ], async (req, res) => { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 25d7070f..204d4468 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -21,7 +21,7 @@ export const App = () => { setLoading(true) fetch(`${BASE_URL}/messages`, { headers: { - // Skickar med användarens JWT-token till backend + // Skickar med användarens JWT-token till backend för att få godkännt att hämta meddelanden Authorization: `Bearer ${user.response.accessToken}`, }, }) diff --git a/package-lock.json b/package-lock.json index 2c710ff5..f8b3722f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2,5 +2,932 @@ "name": "yh-message-app-fullstack", "lockfileVersion": 3, "requires": true, - "packages": {} + "packages": { + "": { + "dependencies": { + "express-rate-limit": "^8.5.2" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT", + "peer": true + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "peer": true + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT", + "peer": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC", + "peer": true + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "peer": true + } + } } diff --git a/package.json b/package.json new file mode 100644 index 00000000..ed511c4b --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "express-rate-limit": "^8.5.2" + } +} From db2c9e89e80ece5b82b39c3f5a7ba5321d54a34a Mon Sep 17 00:00:00 2001 From: haxxmonkey-ship-it Date: Wed, 3 Jun 2026 15:41:39 +0200 Subject: [PATCH 23/48] Adderade Express-Validator (param) --- backend/server.js | 48 ++++++++++++++++++-------------------- frontend/package-lock.json | 29 ----------------------- frontend/src/App.jsx | 3 ++- frontend/src/api.js | 4 +++- 4 files changed, 28 insertions(+), 56 deletions(-) diff --git a/backend/server.js b/backend/server.js index 49558b4f..aa474002 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,6 +1,6 @@ import "dotenv/config" import helmet from "helmet" -import cors from "cors" +import cors from "cors" //ändra så bara vår frontend kan göra requests, istället för att tillåta alla origins. Det ökar säkerheten genom att begränsa vilka domäner som kan interagera med vår backend. import express from "express" import mongoose from "mongoose" import bcrypt from "bcrypt" @@ -10,7 +10,7 @@ import { User } from "./models/User.js" import { authenticateUser } from "./middleware/auth.js" import "./config/db.js" import listEndpoints from "express-list-endpoints" -import { body, validationResult } from "express-validator" // Importerade express-validator för ökad validering av indata +import { body, param, validationResult } from "express-validator" // Importerade express-validator för ökad validering av indata if (!process.env.JWT_SECRET) throw new Error("JWT_SECRET is not set in .env") @@ -18,7 +18,7 @@ const PORT = process.env.PORT || "3000" const app = express() app.use(helmet()) app.use(cors({ - origin: "*", + origin: process.env.FRONTEND_URL, // Endast tillåta requests från vår frontend, istället för att tillåta alla origins. Det ökar säkerheten genom att begränsa vilka domäner som kan interagera med vår backend. })) app.use(express.json()) // Vi föreslår att säkra session-tokeln genom att använda HttpOnly cookies istället för att skicka den i JSON-responsen, vilket minskar risken för XSS-attacker. Det här innebär större ändringar i övrig kod för att hantera autentisering och session, vi har valt att inte genomföra detta själva. @@ -54,7 +54,6 @@ app.post("/register", [ const hashedPassword = await bcrypt.hash(password, 10) const user = new User({ username: username.trim(), email, password: hashedPassword }) - await user.save() const accessToken = jwt.sign( { userId: user._id, username: user.username }, @@ -72,10 +71,10 @@ app.post("/register", [ }, }) } catch (error) { + console.error(error) // Loggar det faktiska felet i serverns konsol res.status(400).json({ success: false, - message: "Could not create user", - error: error, + message: "Something went wrong", // Förenklade felmeddelandet som skickas till klienten för att inte exponera potentiellt känslig information. }) } }) @@ -84,7 +83,6 @@ app.post("/login", [ // Validering av indata för login adderad via Express-vali body("login").trim().notEmpty().withMessage("Username or email is required"), body("password").notEmpty().withMessage("Password is required") ], async (req, res) => { - // 1. Kontrollera om valideringen hittade tomma fält const errors = validationResult(req) if (!errors.isEmpty()) { return res.status(400).json({ success: false, message: "Validation failed", errors: errors.array() }) @@ -129,19 +127,14 @@ app.post("/login", [ // Validering av indata för login adderad via Express-vali }, }) } catch (error) { + console.error(error) //tog bort detaljerad felinformation från svaret för att inte exponera potentiellt känslig information res.status(500).json({ success: false, message: "Something went wrong", - error: error, }) } }) -const isValidId = (id) => mongoose.Types.ObjectId.isValid(id) - - - - @@ -160,11 +153,11 @@ app.get("/messages", authenticateUser, async (req, res) => { //Lade till authent app.post("/messages", [ authenticateUser, - body("message").trim().isLength({ min: 1, max: 500 }).withMessage("Message must be between 1 and 500 characters") + body("message").trim().isLength({ min: 1, max: 250 }).withMessage("Message must be between 1 and 250 characters") ], async (req, res) => { const errors = validationResult(req) if (!errors.isEmpty()) { - return res.status(400).json({ success: false, errors: errors.array() }) +return res.status(400).json({ success: false, error: "Message must be between 1 and 250 characters", errors: errors.array() }) //Adderade mer detaljerad information om valideringsfel i svaret. } try { @@ -178,21 +171,19 @@ app.post("/messages", [ app.patch("/messages/:id", [ authenticateUser, - body("editedMessage").trim().isLength({ min: 1, max: 250 }).withMessage("Edited message must be between 1 and 250 characters") //Adderade validering för det redigerade meddelandet, så att det inte kan vara tomt eller för långt. + param("id").isMongoId().withMessage("Invalid database ID"), + body("editedMessage").trim().isLength({ min: 1, max: 250 }).withMessage("Edited message must be between 1 and 250 characters") ], async (req, res) => { - if (!isValidId(req.params.id)) return res.status(400).json({ error: "Invalid message ID" }) - - const errors = validationResult(req) // Lade const errors efter ID-kontrollen, så att vi inte gör onödiga databasförfrågningar om ID:t redan är ogiltigt. Det gör också att vi kan returnera mer specifika felmeddelanden till klienten, antingen om ID:t är ogiltigt eller om det finns problem med det redigerade meddelandet. + const errors = validationResult(req) if (!errors.isEmpty()) { - return res.status(400).json({ success: false, errors: errors.array() }) + return res.status(400).json({ success: false, error: "Edited message must be between 1 and 250 characters", errors: errors.array() }) //Adderade mer detaljerad information om valideringsfel i svaret. } - try { const message = await Message.findById(req.params.id) if (!message) return res.status(404).json({ error: "Message not found" }) - if (message.user.toString() !== req.user._id.toString()) { + if (!message.user || message.user.toString() !== req.user._id.toString()) { return res.status(403).json({ error: "You can only edit your own messages" }) } @@ -205,13 +196,20 @@ app.patch("/messages/:id", [ } }) -app.delete("/messages/:id", authenticateUser, async (req, res) => { //Lade till authenticateUser för att säkerställa att endast inloggade användare kan radera meddelanden, och att de endast kan radera sina egna meddelanden. - if (!isValidId(req.params.id)) return res.status(400).json({ error: "Invalid message ID" }) +app.delete("/messages/:id", [ + authenticateUser, + param("id").isMongoId().withMessage("Invalid database ID") +], async (req, res) => { + + const errors = validationResult(req) + if (!errors.isEmpty()) { +return res.status(400).json({ success: false, error: "Invalid database ID", errors: errors.array() }) //Adderade mer detaljerad information om valideringsfel i svaret. +} try { const message = await Message.findById(req.params.id) if (!message) return res.status(404).json({ error: "Message not found" }) - if (message.user.toString() !== req.user._id.toString()) { + if (!message.user || message.user.toString() !== req.user._id.toString()) { return res.status(403).json({ error: "You can only delete your own messages" }) } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index aa9ce4b7..e43d824e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.0", "dependencies": { "date-fns": "^2.30.0", - "express-validator": "^7.3.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -2066,19 +2065,6 @@ "node": ">=0.10.0" } }, - "node_modules/express-validator": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.2.tgz", - "integrity": "sha512-ctLw1Vl6dXVH62dIQMDdTAQkrh480mkFuG6/SGXOaVlwPNukhRAe7EgJIMJ2TSAni8iwHBRp530zAZE5ZPF2IA==", - "license": "MIT", - "dependencies": { - "lodash": "^4.18.1", - "validator": "~13.15.23" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3117,12 +3103,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4325,15 +4305,6 @@ "punycode": "^2.1.0" } }, - "node_modules/validator": { - "version": "13.15.35", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", - "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/vite": { "version": "4.5.14", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 25d7070f..a94a2035 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -47,7 +47,8 @@ export const App = () => { }, [user]) const addNewPost = (newMessage) => { - setMessageList([newMessage, ...messageList]) + setMessageList((prevList) => [newMessage, ...prevList]) //prevList säkerställer att inga meddelanden försvinner om användaren klickar snabbt + } const handleUnauthorized = () => { diff --git a/frontend/src/api.js b/frontend/src/api.js index ee6f3531..c5ed351c 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1 +1,3 @@ -export const BASE_URL = "https://yh-message-app-fullstack.onrender.com" +export const BASE_URL = "http://localhost:3000" + +// export const BASE_URL = "https://yh-message-app-fullstack.onrender.com" From 09eefb21a1193a90527744cdbab4661de04e0e7c Mon Sep 17 00:00:00 2001 From: haxxmonkey-ship-it Date: Wed, 3 Jun 2026 15:51:17 +0200 Subject: [PATCH 24/48] Adderade loginLimiter --- backend/middleware/{loginLiminter.js => loginLimiter.js} | 2 -- backend/server.js | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) rename backend/middleware/{loginLiminter.js => loginLimiter.js} (83%) diff --git a/backend/middleware/loginLiminter.js b/backend/middleware/loginLimiter.js similarity index 83% rename from backend/middleware/loginLiminter.js rename to backend/middleware/loginLimiter.js index c50524d2..9b19ee27 100644 --- a/backend/middleware/loginLiminter.js +++ b/backend/middleware/loginLimiter.js @@ -1,5 +1,3 @@ -import rateLimit from "express-rate-limit"; - export const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minuter max: 5, diff --git a/backend/server.js b/backend/server.js index b6b7fefd..68fd7ba8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -10,7 +10,9 @@ import { User } from "./models/User.js" import { authenticateUser } from "./middleware/auth.js" import "./config/db.js" import listEndpoints from "express-list-endpoints" -import { body, validationResult } from "express-validator" // Importerade express-validator för ökad validering av indata +import { body, param, validationResult } from "express-validator" // Importerade express-validator för ökad validering av indata +import rateLimit from "express-rate-limit" // Importerade express-rate-limit för att skydda inloggningsendpointen mot brute-force attacker + if (!process.env.JWT_SECRET) throw new Error("JWT_SECRET is not set in .env") From c666cb937ae8a3e7871d4c9146e5baa7d068d43a Mon Sep 17 00:00:00 2001 From: haxxmonkey-ship-it Date: Fri, 5 Jun 2026 10:22:04 +0200 Subject: [PATCH 25/48] =?UTF-8?q?=C3=85tg=C3=A4rdar=20felkoder=20i=20serve?= =?UTF-8?q?r.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/middleware/loginLimiter.js | 2 ++ backend/server.js | 15 ++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/backend/middleware/loginLimiter.js b/backend/middleware/loginLimiter.js index 9b19ee27..1fe59016 100644 --- a/backend/middleware/loginLimiter.js +++ b/backend/middleware/loginLimiter.js @@ -1,3 +1,5 @@ +import rateLimit from "express-rate-limit" + export const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minuter max: 5, diff --git a/backend/server.js b/backend/server.js index 68fd7ba8..958a747c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -8,10 +8,10 @@ import jwt from "jsonwebtoken" import { Message } from "./models/Message.js" import { User } from "./models/User.js" import { authenticateUser } from "./middleware/auth.js" +import { loginLimiter } from "./middleware/loginLimiter.js" // Importerade loginLimiter-middleware för att skydda inloggningsendpointen mot brute-force attacker import "./config/db.js" import listEndpoints from "express-list-endpoints" import { body, param, validationResult } from "express-validator" // Importerade express-validator för ökad validering av indata -import rateLimit from "express-rate-limit" // Importerade express-rate-limit för att skydda inloggningsendpointen mot brute-force attacker if (!process.env.JWT_SECRET) throw new Error("JWT_SECRET is not set in .env") @@ -48,7 +48,7 @@ app.post("/register", [ if (existingUser) { const field = existingUser.email === email ? "email" : "username" - return res.status(400).json({ + return res.status(400).json({ success: false, message: `A user with this ${field} already exists` }) @@ -74,7 +74,7 @@ app.post("/register", [ }) } catch (error) { console.error(error) // Loggar det faktiska felet i serverns konsol - res.status(400).json({ + res.status(500).json({ //Ändrat från 400 till 500 för att bättre reflektera att det är ett serverfel, inte ett klientfel. success: false, message: "Something went wrong", // Förenklade felmeddelandet som skickas till klienten för att inte exponera potentiellt känslig information. }) @@ -167,7 +167,8 @@ return res.status(400).json({ success: false, error: "Message must be between 1 const saved = await message.save() res.status(201).json(saved) } catch (err) { - res.status(400).json({ message: "Could not save message", errors: err.errors }) + console.error(err) // Loggar det faktiska felet i serverns konsol + res.status(500).json({ message: "Could not save message", errors: err.errors }) } }) @@ -194,7 +195,7 @@ app.patch("/messages/:id", [ const updated = await message.populate("user", "username") res.json(updated) } catch (error) { - res.status(400).json({ error: "Could not update message" }) + res.status(500).json({ error: "Could not update message" }) //Ändrat från 400 till 500 för att bättre reflektera att det är ett serverfel, inte ett klientfel. } }) @@ -205,7 +206,7 @@ app.delete("/messages/:id", [ const errors = validationResult(req) if (!errors.isEmpty()) { -return res.status(400).json({ success: false, error: "Invalid database ID", errors: errors.array() }) //Adderade mer detaljerad information om valideringsfel i svaret. +return res.status(400).json({ success: false, error: "Invalid database ID", errors: errors.array() }) } try { const message = await Message.findById(req.params.id) @@ -218,7 +219,7 @@ return res.status(400).json({ success: false, error: "Invalid database ID", erro await message.deleteOne() res.status(204).send() } catch (error) { - res.status(400).json({ error: "Could not delete message" }) + res.status(500).json({ error: "Could not delete message" }) } }) From fc5779f1871b6073194804e016c431be219de339 Mon Sep 17 00:00:00 2001 From: haxxmonkey-ship-it Date: Fri, 5 Jun 2026 10:35:11 +0200 Subject: [PATCH 26/48] Edit --- backend/server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/server.js b/backend/server.js index 958a747c..e29a187c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -168,7 +168,8 @@ return res.status(400).json({ success: false, error: "Message must be between 1 res.status(201).json(saved) } catch (err) { console.error(err) // Loggar det faktiska felet i serverns konsol - res.status(500).json({ message: "Could not save message", errors: err.errors }) + res.status(500).json({ message: "Could not save message" }) //Ändrat till status 500 och dolt err.errors för att förhindra informationsläckage av databasstrukturen (Information Disclosure). + } }) From e8da7598ddcea488fa013589aea52f499438133d Mon Sep 17 00:00:00 2001 From: linnhass Date: Fri, 5 Jun 2026 10:41:35 +0200 Subject: [PATCH 27/48] =?UTF-8?q?Validering=20i=20inputf=C3=A4lt=20login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/server.js b/backend/server.js index e29a187c..ecc2378f 100644 --- a/backend/server.js +++ b/backend/server.js @@ -82,8 +82,8 @@ app.post("/register", [ }) app.post("/login", loginLimiter, [ // Validering av indata för login adderad via Express-validator - body("login").trim().notEmpty().withMessage("Username or email is required"), - body("password").notEmpty().withMessage("Password is required") + body("login").trim().notEmpty().isLength({ max: 254 }).withMessage("Username or email is required"), // Validerar att login-fältet inte är tomt och inte överstiger 254 tecken (maxlängd för email enligt RFC 5321) + body("password").notEmpty().isLength({ min: 6, max: 64 }).withMessage("Password is required") // Validerar att password-fältet inte är tomt och har en rimlig längd (minst 6 tecken, max 64 tecken) ], async (req, res) => { const errors = validationResult(req) if (!errors.isEmpty()) { From 5a5fef48457030856818df4a58102822ecd9424c Mon Sep 17 00:00:00 2001 From: linnhass Date: Fri, 5 Jun 2026 10:49:56 +0200 Subject: [PATCH 28/48] =?UTF-8?q?=C3=A4ndrat=20felmeddelande?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/server.js b/backend/server.js index ecc2378f..2cc56624 100644 --- a/backend/server.js +++ b/backend/server.js @@ -83,7 +83,7 @@ app.post("/register", [ app.post("/login", loginLimiter, [ // Validering av indata för login adderad via Express-validator body("login").trim().notEmpty().isLength({ max: 254 }).withMessage("Username or email is required"), // Validerar att login-fältet inte är tomt och inte överstiger 254 tecken (maxlängd för email enligt RFC 5321) - body("password").notEmpty().isLength({ min: 6, max: 64 }).withMessage("Password is required") // Validerar att password-fältet inte är tomt och har en rimlig längd (minst 6 tecken, max 64 tecken) + body("password").notEmpty().isLength({ min: 6, max: 64 }).withMessage("Password is required and must be between 6 and 64 characters") // Validerar att password-fältet inte är tomt och har en rimlig längd (minst 6 tecken, max 64 tecken) ], async (req, res) => { const errors = validationResult(req) if (!errors.isEmpty()) { From da341dc9cbeea5befaa7e72d5116059692858bf9 Mon Sep 17 00:00:00 2001 From: haxxmonkey-ship-it Date: Fri, 5 Jun 2026 11:12:26 +0200 Subject: [PATCH 29/48] =?UTF-8?q?=C3=84ndrade=20m=C3=A5svinge=20i=20single?= =?UTF-8?q?message.jsx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/SingleMessage.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/SingleMessage.jsx b/frontend/src/components/SingleMessage.jsx index e381718e..08aa05a8 100644 --- a/frontend/src/components/SingleMessage.jsx +++ b/frontend/src/components/SingleMessage.jsx @@ -22,7 +22,9 @@ export const SingleMessage = ({ message, user, onUnauthorized, fetchPosts }) => return } - await fetchPosts() + if (res.ok) { + await fetchPosts() + } } catch (error) { console.error(error) } @@ -115,4 +117,4 @@ export const SingleMessage = ({ message, user, onUnauthorized, fetchPosts }) =>
) -} +} \ No newline at end of file From c0d1b6887de7c219d68379ac19fe751b13ccf60b Mon Sep 17 00:00:00 2001 From: haxxmonkey-ship-it Date: Fri, 5 Jun 2026 11:43:13 +0200 Subject: [PATCH 30/48] Tog bort felaktig kommentar --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 2a5ed32f..8b43954e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite --host", + "dev": "vite", "build": "vite build", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" From 3a4ea7ee0caed0d77de75f03473b5ca6ba53e97f Mon Sep 17 00:00:00 2001 From: linnhass Date: Fri, 5 Jun 2026 11:49:10 +0200 Subject: [PATCH 31/48] Justerat siffor --- backend/models/User.js | 2 +- backend/server.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/models/User.js b/backend/models/User.js index bb456d4c..646b384c 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -5,7 +5,7 @@ const userSchema = new mongoose.Schema({ type: String, required: true, unique: true, - minlength: 6, // Minimum username length of 6 characters + minlength: 10, // Minimum username length of 10 characters trim: true, }, email: { diff --git a/backend/server.js b/backend/server.js index 2cc56624..9cc5a218 100644 --- a/backend/server.js +++ b/backend/server.js @@ -83,7 +83,7 @@ app.post("/register", [ app.post("/login", loginLimiter, [ // Validering av indata för login adderad via Express-validator body("login").trim().notEmpty().isLength({ max: 254 }).withMessage("Username or email is required"), // Validerar att login-fältet inte är tomt och inte överstiger 254 tecken (maxlängd för email enligt RFC 5321) - body("password").notEmpty().isLength({ min: 6, max: 64 }).withMessage("Password is required and must be between 6 and 64 characters") // Validerar att password-fältet inte är tomt och har en rimlig längd (minst 6 tecken, max 64 tecken) + body("password").notEmpty().isLength({ min: 10, max: 64 }).withMessage("Password is required and must be between 10 and 64 characters") // Validerar att password-fältet inte är tomt och har en rimlig längd (minst 10 tecken, max 64 tecken) ], async (req, res) => { const errors = validationResult(req) if (!errors.isEmpty()) { From 230c31891d7f15aacbffc2d9863c1ac9f80380c1 Mon Sep 17 00:00:00 2001 From: haxxmonkey-ship-it Date: Fri, 5 Jun 2026 12:11:44 +0200 Subject: [PATCH 32/48] =?UTF-8?q?=C3=85tg=C3=A4rdade=20felaktigheter=20i?= =?UTF-8?q?=20authmodal.jsx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 199 +++++++++++----------------------------------- 1 file changed, 47 insertions(+), 152 deletions(-) diff --git a/backend/server.js b/backend/server.js index 2cc56624..0ddd8cea 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,6 +1,6 @@ import "dotenv/config" import helmet from "helmet" -import cors from "cors" //ändra så bara vår frontend kan göra requests, istället för att tillåta alla origins. Det ökar säkerheten genom att begränsa vilka domäner som kan interagera med vår backend. +import cors from "cors" import express from "express" import mongoose from "mongoose" import bcrypt from "bcrypt" @@ -8,222 +8,117 @@ import jwt from "jsonwebtoken" import { Message } from "./models/Message.js" import { User } from "./models/User.js" import { authenticateUser } from "./middleware/auth.js" -import { loginLimiter } from "./middleware/loginLimiter.js" // Importerade loginLimiter-middleware för att skydda inloggningsendpointen mot brute-force attacker +import { loginLimiter } from "./middleware/loginLimiter.js" import "./config/db.js" import listEndpoints from "express-list-endpoints" -import { body, param, validationResult } from "express-validator" // Importerade express-validator för ökad validering av indata - +import { body, param, validationResult } from "express-validator" if (!process.env.JWT_SECRET) throw new Error("JWT_SECRET is not set in .env") const PORT = process.env.PORT || "3000" const app = express() app.use(helmet()) -app.use(cors({ - origin: process.env.FRONTEND_URL, // Endast tillåta requests från vår frontend, istället för att tillåta alla origins. Det ökar säkerheten genom att begränsa vilka domäner som kan interagera med vår backend. -})) +app.use(cors({ origin: process.env.FRONTEND_URL })) app.use(express.json()) -// Vi föreslår att säkra session-tokeln genom att använda HttpOnly cookies istället för att skicka den i JSON-responsen, vilket minskar risken för XSS-attacker. Det här innebär större ändringar i övrig kod för att hantera autentisering och session, vi har valt att inte genomföra detta själva. app.get("/", (req, res) => { res.send(listEndpoints(app)) }) app.post("/register", [ - body("username").isLength({ min: 6, max: 25 }).withMessage("Username must be between 6 and 25 characters"), // Adderade validering för användarnamn - body("email").isEmail().normalizeEmail(), // Validerar att email är i korrekt format och normaliserar det - body("password").isLength({ min: 10, max: 64 }).withMessage("Password must be between 10 and 64 characters") // Adderade validering för lösenordslängd + body("username").isLength({ min: 6, max: 25 }), + body("email").isEmail().normalizeEmail(), + body("password").isLength({ min: 10, max: 64 }) ], async (req, res) => { const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(400).json({ success: false, message: "Failed to register. Check your user name and password.", errors: errors.array() }) // Förenklade felmeddelandet och inkluderade detaljerad information om valideringsfel i svaret. - } + if (!errors.isEmpty()) return res.status(400).json({ success: false, message: "Invalid input" }) try { const { email, password, username } = req.body + const existingUser = await User.findOne({ $or: [{ email: email }, { username: username.trim() }] }) + if (existingUser) return res.status(400).json({ success: false, message: "User already exists" }) - const existingUser = await User.findOne({ - $or: [{ email: email }, { username: username.trim() }] - }) + const hashedPassword = await bcrypt.hash(password, 10) + const user = new User({ username: username.trim(), email, password: hashedPassword }) + await user.save() - if (existingUser) { - const field = existingUser.email === email ? "email" : "username" - return res.status(400).json({ - success: false, - message: `A user with this ${field} already exists` - }) - } - - const hashedPassword = await bcrypt.hash(password, 10) - const user = new User({ username: username.trim(), email, password: hashedPassword }) - - const accessToken = jwt.sign( - { userId: user._id, username: user.username }, - process.env.JWT_SECRET, - { expiresIn: "1h" } // Sänkte token expiration time till 1 timme för ökad säkerhet - ) - - res.status(201).json({ - success: true, - message: "User created successfully", - response: { - username: user.username, - id: user._id, - accessToken, - }, - }) - } catch (error) { - console.error(error) // Loggar det faktiska felet i serverns konsol - res.status(500).json({ //Ändrat från 400 till 500 för att bättre reflektera att det är ett serverfel, inte ett klientfel. - success: false, - message: "Something went wrong", // Förenklade felmeddelandet som skickas till klienten för att inte exponera potentiellt känslig information. - }) - } -}) + const accessToken = jwt.sign({ userId: user._id, username: user.username }, process.env.JWT_SECRET, { expiresIn: "1h" }) + res.status(201).json({ success: true, response: { username: user.username, id: user._id, accessToken } }) + } catch (error) { + res.status(500).json({ success: false, message: "Something went wrong" }) + } +}) -app.post("/login", loginLimiter, [ // Validering av indata för login adderad via Express-validator - body("login").trim().notEmpty().isLength({ max: 254 }).withMessage("Username or email is required"), // Validerar att login-fältet inte är tomt och inte överstiger 254 tecken (maxlängd för email enligt RFC 5321) - body("password").notEmpty().isLength({ min: 6, max: 64 }).withMessage("Password is required and must be between 6 and 64 characters") // Validerar att password-fältet inte är tomt och har en rimlig längd (minst 6 tecken, max 64 tecken) +app.post("/login", loginLimiter, [ + body("login").trim().notEmpty(), + body("password").notEmpty() ], async (req, res) => { const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(400).json({ success: false, message: "Validation failed", errors: errors.array() }) - } + if (!errors.isEmpty()) return res.status(400).json({ success: false, message: "Validation failed" }) try { const { login, password } = req.body const user = await User.findOne({ - $or: [{ username: login }, { email: login }] + $or: [ + { username: { $regex: new RegExp(`^${login}$`, 'i') } }, + { email: login.toLowerCase() } + ] }) - if (!user) { - return res.status(401).json({ - success: false, - message: "No account found with that username or email", - response: null, - }) - } + if (!user) return res.status(401).json({ success: false, message: "No account found" }) const passwordMatch = await bcrypt.compare(password, user.password) - if (!passwordMatch) { - return res.status(401).json({ - success: false, - message: "Password is incorrect", - response: null, - }) - } - - const accessToken = jwt.sign( - { userId: user._id, username: user.username }, - process.env.JWT_SECRET, - { expiresIn: "1h" } //Sänkte token expiration time till 1 timme för ökad säkerhet - ) - - res.json({ - success: true, - message: "Logged in successfully", - response: { - username: user.username, - id: user._id, - accessToken, - }, - }) + if (!passwordMatch) return res.status(401).json({ success: false, message: "Password is incorrect" }) + + const accessToken = jwt.sign({ userId: user._id, username: user.username }, process.env.JWT_SECRET, { expiresIn: "1h" }) + res.json({ success: true, response: { username: user.username, id: user._id, accessToken } }) } catch (error) { - console.error(error) //tog bort detaljerad felinformation från svaret för att inte exponera potentiellt känslig information - res.status(500).json({ - success: false, - message: "Something went wrong", - }) + res.status(500).json({ success: false, message: "Something went wrong" }) } }) - - - -app.get("/messages", authenticateUser, async (req, res) => { //Lade till authenticateUser för att säkerställa att endast inloggade användare kan hämta meddelanden. +app.get("/messages", authenticateUser, async (req, res) => { try { - const messages = await Message.find() - .sort({ createdAt: "desc" }) - .limit(20) - .populate("user", "username") - .exec() + const messages = await Message.find().sort({ createdAt: "desc" }).limit(20).populate("user", "username").exec() res.json(messages) } catch (error) { res.status(500).json({ message: "Could not fetch messages" }) } }) -app.post("/messages", [ - authenticateUser, - body("message").trim().isLength({ min: 1, max: 250 }).withMessage("Message must be between 1 and 250 characters") -], async (req, res) => { - const errors = validationResult(req) - if (!errors.isEmpty()) { -return res.status(400).json({ success: false, error: "Message must be between 1 and 250 characters", errors: errors.array() }) //Adderade mer detaljerad information om valideringsfel i svaret. - } - +app.post("/messages", [authenticateUser, body("message").trim().isLength({ min: 1, max: 250 })], async (req, res) => { try { const message = new Message({ message: req.body.message, user: req.user._id }) const saved = await message.save() res.status(201).json(saved) } catch (err) { - console.error(err) // Loggar det faktiska felet i serverns konsol - res.status(500).json({ message: "Could not save message" }) //Ändrat till status 500 och dolt err.errors för att förhindra informationsläckage av databasstrukturen (Information Disclosure). - + res.status(500).json({ message: "Could not save message" }) } }) -app.patch("/messages/:id", [ - authenticateUser, - param("id").isMongoId().withMessage("Invalid database ID"), - body("editedMessage").trim().isLength({ min: 1, max: 250 }).withMessage("Edited message must be between 1 and 250 characters") -], async (req, res) => { - - const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(400).json({ success: false, error: "Edited message must be between 1 and 250 characters", errors: errors.array() }) //Adderade mer detaljerad information om valideringsfel i svaret. - } +app.patch("/messages/:id", [authenticateUser, param("id").isMongoId(), body("editedMessage").trim().isLength({ min: 1, max: 250 })], async (req, res) => { try { const message = await Message.findById(req.params.id) - if (!message) return res.status(404).json({ error: "Message not found" }) - - if (!message.user || message.user.toString() !== req.user._id.toString()) { - return res.status(403).json({ error: "You can only edit your own messages" }) - } - + if (!message || message.user.toString() !== req.user._id.toString()) return res.status(403).json({ error: "Unauthorized" }) + message.message = req.body.editedMessage await message.save() - const updated = await message.populate("user", "username") - res.json(updated) + res.json(await message.populate("user", "username")) } catch (error) { - res.status(500).json({ error: "Could not update message" }) //Ändrat från 400 till 500 för att bättre reflektera att det är ett serverfel, inte ett klientfel. + res.status(500).json({ error: "Could not update" }) } }) -app.delete("/messages/:id", [ - authenticateUser, - param("id").isMongoId().withMessage("Invalid database ID") -], async (req, res) => { - - const errors = validationResult(req) - if (!errors.isEmpty()) { -return res.status(400).json({ success: false, error: "Invalid database ID", errors: errors.array() }) -} +app.delete("/messages/:id", [authenticateUser, param("id").isMongoId()], async (req, res) => { try { const message = await Message.findById(req.params.id) - if (!message) return res.status(404).json({ error: "Message not found" }) - - if (!message.user || message.user.toString() !== req.user._id.toString()) { - return res.status(403).json({ error: "You can only delete your own messages" }) - } - + if (!message || message.user.toString() !== req.user._id.toString()) return res.status(403).json({ error: "Unauthorized" }) + await message.deleteOne() res.status(204).send() } catch (error) { - res.status(500).json({ error: "Could not delete message" }) + res.status(500).json({ error: "Could not delete" }) } }) -app.listen(PORT, () => { - console.log(`Listening on port ${PORT}`) -}) +app.listen(PORT, () => console.log(`Listening on port ${PORT}`)) \ No newline at end of file From 0e599c6d04f793bf5f726d2d4fa9f5dc74146bc0 Mon Sep 17 00:00:00 2001 From: linnhass Date: Fri, 5 Jun 2026 14:20:06 +0200 Subject: [PATCH 33/48] =?UTF-8?q?L=C3=B6ste=20merge=20konflikt=20i=20serve?= =?UTF-8?q?r.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/backend/server.js b/backend/server.js index 38997624..0ac7a52b 100644 --- a/backend/server.js +++ b/backend/server.js @@ -49,11 +49,6 @@ app.post("/register", [ } }) -<<<<<<< HEAD -app.post("/login", loginLimiter, [ - body("login").trim().notEmpty(), - body("password").notEmpty() -======= const hashedPassword = await bcrypt.hash(password, 10) const user = new User({ username: username.trim(), email, password: hashedPassword }) @@ -84,7 +79,6 @@ app.post("/login", loginLimiter, [ app.post("/login", loginLimiter, [ // Validering av indata för login adderad via Express-validator body("login").trim().notEmpty().isLength({ max: 254 }).withMessage("Username or email is required"), // Validerar att login-fältet inte är tomt och inte överstiger 254 tecken (maxlängd för email enligt RFC 5321) body("password").notEmpty().isLength({ min: 10, max: 64 }).withMessage("Password is required and must be between 10 and 64 characters") // Validerar att password-fältet inte är tomt och har en rimlig längd (minst 10 tecken, max 64 tecken) ->>>>>>> 3a4ea7ee0caed0d77de75f03473b5ca6ba53e97f ], async (req, res) => { const errors = validationResult(req) if (!errors.isEmpty()) return res.status(400).json({ success: false, message: "Validation failed" }) From 06e9a5ff2121315766390d704309ea18b6f6e414 Mon Sep 17 00:00:00 2001 From: linnhass Date: Fri, 5 Jun 2026 14:24:43 +0200 Subject: [PATCH 34/48] Lagt till parametrar i User.js --- backend/middleware/loginLimiter.js | 2 +- backend/models/User.js | 5 ++++- backend/server.js | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/middleware/loginLimiter.js b/backend/middleware/loginLimiter.js index 1fe59016..a55443c3 100644 --- a/backend/middleware/loginLimiter.js +++ b/backend/middleware/loginLimiter.js @@ -1,4 +1,4 @@ -import rateLimit from "express-rate-limit" +import rateLimit from "express-rate-limit" // Express-rate-limit används för att begränsa antalet inloggningsförsök från en specifik IP-adress under en viss tidsperiod. Detta hjälper till att skydda mot brute-force attacker där en angripare försöker gissa lösenord genom att göra många inloggningsförsök. export const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minuter diff --git a/backend/models/User.js b/backend/models/User.js index 646b384c..66905d49 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -5,7 +5,7 @@ const userSchema = new mongoose.Schema({ type: String, required: true, unique: true, - minlength: 10, // Minimum username length of 10 characters + maxlength: 25, // maxlängd på användarnamn satt till 25 tecken för att undvika överdrivet långa användarnamn trim: true, }, email: { @@ -13,10 +13,13 @@ const userSchema = new mongoose.Schema({ required: true, unique: true, lowercase: true, + maxlength: 254, // maxlängd på email satt till 254 tecken enligt RFC 5321 }, password: { type: String, required: true, + minlength: 10, // minlängd på lösenord satt till 10 tecken för att uppmuntra starka lösenord + maxlength: 64, // maxlängd på lösenord satt till 64 tecken för att undvika överdrivet långa lösenord }, }) diff --git a/backend/server.js b/backend/server.js index 0ac7a52b..46413f16 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,5 +1,5 @@ import "dotenv/config" -import helmet from "helmet" +import helmet from "helmet" // Helmet hjälper till att säkra Express-applikationen genom att ställa in olika HTTP-headers som skyddar mot vanliga sårbarheter. import cors from "cors" import express from "express" import mongoose from "mongoose" From b34436b5354d695deeac9874cdd1f8af61c6386b Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Mon, 8 Jun 2026 08:39:30 +0200 Subject: [PATCH 35/48] =?UTF-8?q?=C3=85tg=C3=A4rdade=20misstag=20i=20koden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 170 +++++++++++++++++++++++++++++++++------------- 1 file changed, 123 insertions(+), 47 deletions(-) diff --git a/backend/server.js b/backend/server.js index 46413f16..68fd7ba8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,6 +1,6 @@ import "dotenv/config" -import helmet from "helmet" // Helmet hjälper till att säkra Express-applikationen genom att ställa in olika HTTP-headers som skyddar mot vanliga sårbarheter. -import cors from "cors" +import helmet from "helmet" +import cors from "cors" //ändra så bara vår frontend kan göra requests, istället för att tillåta alla origins. Det ökar säkerheten genom att begränsa vilka domäner som kan interagera med vår backend. import express from "express" import mongoose from "mongoose" import bcrypt from "bcrypt" @@ -8,46 +8,51 @@ import jwt from "jsonwebtoken" import { Message } from "./models/Message.js" import { User } from "./models/User.js" import { authenticateUser } from "./middleware/auth.js" -import { loginLimiter } from "./middleware/loginLimiter.js" import "./config/db.js" import listEndpoints from "express-list-endpoints" -import { body, param, validationResult } from "express-validator" +import { body, param, validationResult } from "express-validator" // Importerade express-validator för ökad validering av indata +import rateLimit from "express-rate-limit" // Importerade express-rate-limit för att skydda inloggningsendpointen mot brute-force attacker + if (!process.env.JWT_SECRET) throw new Error("JWT_SECRET is not set in .env") const PORT = process.env.PORT || "3000" const app = express() app.use(helmet()) -app.use(cors({ origin: process.env.FRONTEND_URL })) +app.use(cors({ + origin: process.env.FRONTEND_URL, // Endast tillåta requests från vår frontend, istället för att tillåta alla origins. Det ökar säkerheten genom att begränsa vilka domäner som kan interagera med vår backend. +})) app.use(express.json()) +// Vi föreslår att säkra session-tokeln genom att använda HttpOnly cookies istället för att skicka den i JSON-responsen, vilket minskar risken för XSS-attacker. Det här innebär större ändringar i övrig kod för att hantera autentisering och session, vi har valt att inte genomföra detta själva. app.get("/", (req, res) => { res.send(listEndpoints(app)) }) app.post("/register", [ - body("username").isLength({ min: 6, max: 25 }), - body("email").isEmail().normalizeEmail(), - body("password").isLength({ min: 10, max: 64 }) + body("username").isLength({ min: 6, max: 25 }).withMessage("Username must be between 6 and 25 characters"), // Adderade validering för användarnamn + body("email").isEmail().normalizeEmail(), // Validerar att email är i korrekt format och normaliserar det + body("password").isLength({ min: 10, max: 64 }).withMessage("Password must be between 10 and 64 characters") // Adderade validering för lösenordslängd ], async (req, res) => { const errors = validationResult(req) - if (!errors.isEmpty()) return res.status(400).json({ success: false, message: "Invalid input" }) + if (!errors.isEmpty()) { + return res.status(400).json({ success: false, message: "Failed to register. Check your user name and password.", errors: errors.array() }) // Förenklade felmeddelandet och inkluderade detaljerad information om valideringsfel i svaret. + } try { const { email, password, username } = req.body - const existingUser = await User.findOne({ $or: [{ email: email }, { username: username.trim() }] }) - if (existingUser) return res.status(400).json({ success: false, message: "User already exists" }) - const hashedPassword = await bcrypt.hash(password, 10) - const user = new User({ username: username.trim(), email, password: hashedPassword }) - await user.save() + const existingUser = await User.findOne({ + $or: [{ email: email }, { username: username.trim() }] + }) - const accessToken = jwt.sign({ userId: user._id, username: user.username }, process.env.JWT_SECRET, { expiresIn: "1h" }) - res.status(201).json({ success: true, response: { username: user.username, id: user._id, accessToken } }) - } catch (error) { - res.status(500).json({ success: false, message: "Something went wrong" }) - } -}) + if (existingUser) { + const field = existingUser.email === email ? "email" : "username" + return res.status(400).json({ + success: false, + message: `A user with this ${field} already exists` + }) + } const hashedPassword = await bcrypt.hash(password, 10) const user = new User({ username: username.trim(), email, password: hashedPassword }) @@ -69,7 +74,7 @@ app.post("/register", [ }) } catch (error) { console.error(error) // Loggar det faktiska felet i serverns konsol - res.status(500).json({ //Ändrat från 400 till 500 för att bättre reflektera att det är ett serverfel, inte ett klientfel. + res.status(400).json({ success: false, message: "Something went wrong", // Förenklade felmeddelandet som skickas till klienten för att inte exponera potentiellt känslig information. }) @@ -77,75 +82,146 @@ app.post("/register", [ }) app.post("/login", loginLimiter, [ // Validering av indata för login adderad via Express-validator - body("login").trim().notEmpty().isLength({ max: 254 }).withMessage("Username or email is required"), // Validerar att login-fältet inte är tomt och inte överstiger 254 tecken (maxlängd för email enligt RFC 5321) - body("password").notEmpty().isLength({ min: 10, max: 64 }).withMessage("Password is required and must be between 10 and 64 characters") // Validerar att password-fältet inte är tomt och har en rimlig längd (minst 10 tecken, max 64 tecken) + body("login").trim().notEmpty().withMessage("Username or email is required"), + body("password").notEmpty().withMessage("Password is required") ], async (req, res) => { const errors = validationResult(req) - if (!errors.isEmpty()) return res.status(400).json({ success: false, message: "Validation failed" }) + if (!errors.isEmpty()) { + return res.status(400).json({ success: false, message: "Validation failed", errors: errors.array() }) + } try { const { login, password } = req.body const user = await User.findOne({ - $or: [ - { username: { $regex: new RegExp(`^${login}$`, 'i') } }, - { email: login.toLowerCase() } - ] + $or: [{ username: login }, { email: login }] }) - if (!user) return res.status(401).json({ success: false, message: "No account found" }) + if (!user) { + return res.status(401).json({ + success: false, + message: "No account found with that username or email", + response: null, + }) + } const passwordMatch = await bcrypt.compare(password, user.password) - if (!passwordMatch) return res.status(401).json({ success: false, message: "Password is incorrect" }) + if (!passwordMatch) { + return res.status(401).json({ + success: false, + message: "Password is incorrect", + response: null, + }) + } + + const accessToken = jwt.sign( + { userId: user._id, username: user.username }, + process.env.JWT_SECRET, + { expiresIn: "1h" } //Sänkte token expiration time till 1 timme för ökad säkerhet + ) - const accessToken = jwt.sign({ userId: user._id, username: user.username }, process.env.JWT_SECRET, { expiresIn: "1h" }) - res.json({ success: true, response: { username: user.username, id: user._id, accessToken } }) + res.json({ + success: true, + message: "Logged in successfully", + response: { + username: user.username, + id: user._id, + accessToken, + }, + }) } catch (error) { - res.status(500).json({ success: false, message: "Something went wrong" }) + console.error(error) //tog bort detaljerad felinformation från svaret för att inte exponera potentiellt känslig information + res.status(500).json({ + success: false, + message: "Something went wrong", + }) } }) -app.get("/messages", authenticateUser, async (req, res) => { + + + +app.get("/messages", authenticateUser, async (req, res) => { //Lade till authenticateUser för att säkerställa att endast inloggade användare kan hämta meddelanden. try { - const messages = await Message.find().sort({ createdAt: "desc" }).limit(20).populate("user", "username").exec() + const messages = await Message.find() + .sort({ createdAt: "desc" }) + .limit(20) + .populate("user", "username") + .exec() res.json(messages) } catch (error) { res.status(500).json({ message: "Could not fetch messages" }) } }) -app.post("/messages", [authenticateUser, body("message").trim().isLength({ min: 1, max: 250 })], async (req, res) => { +app.post("/messages", [ + authenticateUser, + body("message").trim().isLength({ min: 1, max: 250 }).withMessage("Message must be between 1 and 250 characters") +], async (req, res) => { + const errors = validationResult(req) + if (!errors.isEmpty()) { +return res.status(400).json({ success: false, error: "Message must be between 1 and 250 characters", errors: errors.array() }) //Adderade mer detaljerad information om valideringsfel i svaret. + } + try { const message = new Message({ message: req.body.message, user: req.user._id }) const saved = await message.save() res.status(201).json(saved) } catch (err) { - res.status(500).json({ message: "Could not save message" }) + res.status(400).json({ message: "Could not save message", errors: err.errors }) } }) -app.patch("/messages/:id", [authenticateUser, param("id").isMongoId(), body("editedMessage").trim().isLength({ min: 1, max: 250 })], async (req, res) => { +app.patch("/messages/:id", [ + authenticateUser, + param("id").isMongoId().withMessage("Invalid database ID"), + body("editedMessage").trim().isLength({ min: 1, max: 250 }).withMessage("Edited message must be between 1 and 250 characters") +], async (req, res) => { + + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(400).json({ success: false, error: "Edited message must be between 1 and 250 characters", errors: errors.array() }) //Adderade mer detaljerad information om valideringsfel i svaret. + } try { const message = await Message.findById(req.params.id) - if (!message || message.user.toString() !== req.user._id.toString()) return res.status(403).json({ error: "Unauthorized" }) - + if (!message) return res.status(404).json({ error: "Message not found" }) + + if (!message.user || message.user.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "You can only edit your own messages" }) + } + message.message = req.body.editedMessage await message.save() - res.json(await message.populate("user", "username")) + const updated = await message.populate("user", "username") + res.json(updated) } catch (error) { - res.status(500).json({ error: "Could not update" }) + res.status(400).json({ error: "Could not update message" }) } }) -app.delete("/messages/:id", [authenticateUser, param("id").isMongoId()], async (req, res) => { +app.delete("/messages/:id", [ + authenticateUser, + param("id").isMongoId().withMessage("Invalid database ID") +], async (req, res) => { + + const errors = validationResult(req) + if (!errors.isEmpty()) { +return res.status(400).json({ success: false, error: "Invalid database ID", errors: errors.array() }) //Adderade mer detaljerad information om valideringsfel i svaret. +} try { const message = await Message.findById(req.params.id) - if (!message || message.user.toString() !== req.user._id.toString()) return res.status(403).json({ error: "Unauthorized" }) - + if (!message) return res.status(404).json({ error: "Message not found" }) + + if (!message.user || message.user.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "You can only delete your own messages" }) + } + await message.deleteOne() res.status(204).send() } catch (error) { - res.status(500).json({ error: "Could not delete" }) + res.status(400).json({ error: "Could not delete message" }) } }) -app.listen(PORT, () => console.log(`Listening on port ${PORT}`)) \ No newline at end of file +app.listen(PORT, () => { + console.log(`Listening on port ${PORT}`) +}) From 1116a7b8f6ec677b8facad9db23e06c470bc2159 Mon Sep 17 00:00:00 2001 From: linnhass Date: Mon, 8 Jun 2026 08:44:18 +0200 Subject: [PATCH 36/48] =?UTF-8?q?=C3=85tg=C3=A4rndat=20felaktig=20merge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 170 +++++++++++++++++++++++++++++++++------------- 1 file changed, 123 insertions(+), 47 deletions(-) diff --git a/backend/server.js b/backend/server.js index 46413f16..6ef707ce 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,6 +1,6 @@ import "dotenv/config" -import helmet from "helmet" // Helmet hjälper till att säkra Express-applikationen genom att ställa in olika HTTP-headers som skyddar mot vanliga sårbarheter. -import cors from "cors" +import helmet from "helmet"// Helmet hjälper till att säkra Express-applikationen genom att ställa in olika HTTP-headers som skyddar mot vanliga sårbarheter. +import cors from "cors" //ändra så bara vår frontend kan göra requests, istället för att tillåta alla origins. Det ökar säkerheten genom att begränsa vilka domäner som kan interagera med vår backend. import express from "express" import mongoose from "mongoose" import bcrypt from "bcrypt" @@ -8,46 +8,51 @@ import jwt from "jsonwebtoken" import { Message } from "./models/Message.js" import { User } from "./models/User.js" import { authenticateUser } from "./middleware/auth.js" -import { loginLimiter } from "./middleware/loginLimiter.js" import "./config/db.js" import listEndpoints from "express-list-endpoints" -import { body, param, validationResult } from "express-validator" +import { body, param, validationResult } from "express-validator" // Importerade express-validator för ökad validering av indata +import rateLimit from "express-rate-limit" // Importerade express-rate-limit för att skydda inloggningsendpointen mot brute-force attacker + if (!process.env.JWT_SECRET) throw new Error("JWT_SECRET is not set in .env") const PORT = process.env.PORT || "3000" const app = express() app.use(helmet()) -app.use(cors({ origin: process.env.FRONTEND_URL })) +app.use(cors({ + origin: process.env.FRONTEND_URL, // Endast tillåta requests från vår frontend, istället för att tillåta alla origins. Det ökar säkerheten genom att begränsa vilka domäner som kan interagera med vår backend. +})) app.use(express.json()) +// Vi föreslår att säkra session-tokeln genom att använda HttpOnly cookies istället för att skicka den i JSON-responsen, vilket minskar risken för XSS-attacker. Det här innebär större ändringar i övrig kod för att hantera autentisering och session, vi har valt att inte genomföra detta själva. app.get("/", (req, res) => { res.send(listEndpoints(app)) }) app.post("/register", [ - body("username").isLength({ min: 6, max: 25 }), - body("email").isEmail().normalizeEmail(), - body("password").isLength({ min: 10, max: 64 }) + body("username").isLength({ min: 6, max: 25 }).withMessage("Username must be between 6 and 25 characters"), // Adderade validering för användarnamn + body("email").isEmail().normalizeEmail(), // Validerar att email är i korrekt format och normaliserar det + body("password").isLength({ min: 10, max: 64 }).withMessage("Password must be between 10 and 64 characters") // Adderade validering för lösenordslängd ], async (req, res) => { const errors = validationResult(req) - if (!errors.isEmpty()) return res.status(400).json({ success: false, message: "Invalid input" }) + if (!errors.isEmpty()) { + return res.status(400).json({ success: false, message: "Failed to register. Check your user name and password.", errors: errors.array() }) // Förenklade felmeddelandet och inkluderade detaljerad information om valideringsfel i svaret. + } try { const { email, password, username } = req.body - const existingUser = await User.findOne({ $or: [{ email: email }, { username: username.trim() }] }) - if (existingUser) return res.status(400).json({ success: false, message: "User already exists" }) - const hashedPassword = await bcrypt.hash(password, 10) - const user = new User({ username: username.trim(), email, password: hashedPassword }) - await user.save() + const existingUser = await User.findOne({ + $or: [{ email: email }, { username: username.trim() }] + }) - const accessToken = jwt.sign({ userId: user._id, username: user.username }, process.env.JWT_SECRET, { expiresIn: "1h" }) - res.status(201).json({ success: true, response: { username: user.username, id: user._id, accessToken } }) - } catch (error) { - res.status(500).json({ success: false, message: "Something went wrong" }) - } -}) + if (existingUser) { + const field = existingUser.email === email ? "email" : "username" + return res.status(400).json({ + success: false, + message: `A user with this ${field} already exists` + }) + } const hashedPassword = await bcrypt.hash(password, 10) const user = new User({ username: username.trim(), email, password: hashedPassword }) @@ -69,7 +74,7 @@ app.post("/register", [ }) } catch (error) { console.error(error) // Loggar det faktiska felet i serverns konsol - res.status(500).json({ //Ändrat från 400 till 500 för att bättre reflektera att det är ett serverfel, inte ett klientfel. + res.status(400).json({ success: false, message: "Something went wrong", // Förenklade felmeddelandet som skickas till klienten för att inte exponera potentiellt känslig information. }) @@ -77,75 +82,146 @@ app.post("/register", [ }) app.post("/login", loginLimiter, [ // Validering av indata för login adderad via Express-validator - body("login").trim().notEmpty().isLength({ max: 254 }).withMessage("Username or email is required"), // Validerar att login-fältet inte är tomt och inte överstiger 254 tecken (maxlängd för email enligt RFC 5321) - body("password").notEmpty().isLength({ min: 10, max: 64 }).withMessage("Password is required and must be between 10 and 64 characters") // Validerar att password-fältet inte är tomt och har en rimlig längd (minst 10 tecken, max 64 tecken) + body("login").trim().notEmpty().withMessage("Username or email is required"), + body("password").notEmpty().withMessage("Password is required") ], async (req, res) => { const errors = validationResult(req) - if (!errors.isEmpty()) return res.status(400).json({ success: false, message: "Validation failed" }) + if (!errors.isEmpty()) { + return res.status(400).json({ success: false, message: "Validation failed", errors: errors.array() }) + } try { const { login, password } = req.body const user = await User.findOne({ - $or: [ - { username: { $regex: new RegExp(`^${login}$`, 'i') } }, - { email: login.toLowerCase() } - ] + $or: [{ username: login }, { email: login }] }) - if (!user) return res.status(401).json({ success: false, message: "No account found" }) + if (!user) { + return res.status(401).json({ + success: false, + message: "No account found with that username or email", + response: null, + }) + } const passwordMatch = await bcrypt.compare(password, user.password) - if (!passwordMatch) return res.status(401).json({ success: false, message: "Password is incorrect" }) + if (!passwordMatch) { + return res.status(401).json({ + success: false, + message: "Password is incorrect", + response: null, + }) + } - const accessToken = jwt.sign({ userId: user._id, username: user.username }, process.env.JWT_SECRET, { expiresIn: "1h" }) - res.json({ success: true, response: { username: user.username, id: user._id, accessToken } }) + const accessToken = jwt.sign( + { userId: user._id, username: user.username }, + process.env.JWT_SECRET, + { expiresIn: "1h" } //Sänkte token expiration time till 1 timme för ökad säkerhet + ) + + res.json({ + success: true, + message: "Logged in successfully", + response: { + username: user.username, + id: user._id, + accessToken, + }, + }) } catch (error) { - res.status(500).json({ success: false, message: "Something went wrong" }) + console.error(error) //tog bort detaljerad felinformation från svaret för att inte exponera potentiellt känslig information + res.status(500).json({ + success: false, + message: "Something went wrong", + }) } }) -app.get("/messages", authenticateUser, async (req, res) => { + + + +app.get("/messages", authenticateUser, async (req, res) => { //Lade till authenticateUser för att säkerställa att endast inloggade användare kan hämta meddelanden. try { - const messages = await Message.find().sort({ createdAt: "desc" }).limit(20).populate("user", "username").exec() + const messages = await Message.find() + .sort({ createdAt: "desc" }) + .limit(20) + .populate("user", "username") + .exec() res.json(messages) } catch (error) { res.status(500).json({ message: "Could not fetch messages" }) } }) -app.post("/messages", [authenticateUser, body("message").trim().isLength({ min: 1, max: 250 })], async (req, res) => { +app.post("/messages", [ + authenticateUser, + body("message").trim().isLength({ min: 1, max: 250 }).withMessage("Message must be between 1 and 250 characters") +], async (req, res) => { + const errors = validationResult(req) + if (!errors.isEmpty()) { +return res.status(400).json({ success: false, error: "Message must be between 1 and 250 characters", errors: errors.array() }) //Adderade mer detaljerad information om valideringsfel i svaret. + } + try { const message = new Message({ message: req.body.message, user: req.user._id }) const saved = await message.save() res.status(201).json(saved) } catch (err) { - res.status(500).json({ message: "Could not save message" }) + res.status(400).json({ message: "Could not save message", errors: err.errors }) } }) -app.patch("/messages/:id", [authenticateUser, param("id").isMongoId(), body("editedMessage").trim().isLength({ min: 1, max: 250 })], async (req, res) => { +app.patch("/messages/:id", [ + authenticateUser, + param("id").isMongoId().withMessage("Invalid database ID"), + body("editedMessage").trim().isLength({ min: 1, max: 250 }).withMessage("Edited message must be between 1 and 250 characters") +], async (req, res) => { + + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(400).json({ success: false, error: "Edited message must be between 1 and 250 characters", errors: errors.array() }) //Adderade mer detaljerad information om valideringsfel i svaret. + } try { const message = await Message.findById(req.params.id) - if (!message || message.user.toString() !== req.user._id.toString()) return res.status(403).json({ error: "Unauthorized" }) - + if (!message) return res.status(404).json({ error: "Message not found" }) + + if (!message.user || message.user.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "You can only edit your own messages" }) + } + message.message = req.body.editedMessage await message.save() - res.json(await message.populate("user", "username")) + const updated = await message.populate("user", "username") + res.json(updated) } catch (error) { - res.status(500).json({ error: "Could not update" }) + res.status(400).json({ error: "Could not update message" }) } }) -app.delete("/messages/:id", [authenticateUser, param("id").isMongoId()], async (req, res) => { +app.delete("/messages/:id", [ + authenticateUser, + param("id").isMongoId().withMessage("Invalid database ID") +], async (req, res) => { + + const errors = validationResult(req) + if (!errors.isEmpty()) { +return res.status(400).json({ success: false, error: "Invalid database ID", errors: errors.array() }) //Adderade mer detaljerad information om valideringsfel i svaret. +} try { const message = await Message.findById(req.params.id) - if (!message || message.user.toString() !== req.user._id.toString()) return res.status(403).json({ error: "Unauthorized" }) - + if (!message) return res.status(404).json({ error: "Message not found" }) + + if (!message.user || message.user.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "You can only delete your own messages" }) + } + await message.deleteOne() res.status(204).send() } catch (error) { - res.status(500).json({ error: "Could not delete" }) + res.status(400).json({ error: "Could not delete message" }) } }) -app.listen(PORT, () => console.log(`Listening on port ${PORT}`)) \ No newline at end of file +app.listen(PORT, () => { + console.log(`Listening on port ${PORT}`) +}) \ No newline at end of file From 7ba17a695601bc7b35f7bd9580d408dac29795bd Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Mon, 8 Jun 2026 08:53:48 +0200 Subject: [PATCH 37/48] =?UTF-8?q?=C3=85tg=C3=A4rdade=20fel=20i=20AuthModal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/AuthModal.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/AuthModal.jsx b/frontend/src/components/AuthModal.jsx index 3d25f46b..94c1ef7b 100644 --- a/frontend/src/components/AuthModal.jsx +++ b/frontend/src/components/AuthModal.jsx @@ -4,10 +4,10 @@ import { BASE_URL } from "../api" export const AuthModal = ({ mode, onClose, onSuccess }) => { const [username, setUsername] = useState("") const [email, setEmail] = useState("") - const [login, setLogin] = useState("") + // Initierar login med username för att säkerställa att inloggningsfältet + // skickar rätt data (login-nyckel) till backend för verifiering. + const [login, setLogin] = useState(username) const [password, setPassword] = useState("") - const [error, setError] = useState(null) - const [submitting, setSubmitting] = useState(false) const handleSubmit = async (e) => { e.preventDefault() From 3ce526da49c299505d1a41d32ffecc534d4a870b Mon Sep 17 00:00:00 2001 From: linnhass Date: Mon, 8 Jun 2026 08:55:58 +0200 Subject: [PATCH 38/48] lagt till kommentar --- backend/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/server.js b/backend/server.js index 68fd7ba8..4c2dea2c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,5 +1,5 @@ import "dotenv/config" -import helmet from "helmet" +import helmet from "helmet" // Helmet hjälper till att säkra Express-applikationen genom att ställa in olika HTTP-headers som skyddar mot vanliga sårbarheter. import cors from "cors" //ändra så bara vår frontend kan göra requests, istället för att tillåta alla origins. Det ökar säkerheten genom att begränsa vilka domäner som kan interagera med vår backend. import express from "express" import mongoose from "mongoose" From 7b4b087c9c560fb75444be7217c96b7646d9c964 Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Mon, 8 Jun 2026 09:44:57 +0200 Subject: [PATCH 39/48] =?UTF-8?q?=C3=85tg=C3=A4rdade=20fel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/server.js b/backend/server.js index 4c2dea2c..cb0d8e0f 100644 --- a/backend/server.js +++ b/backend/server.js @@ -8,11 +8,10 @@ import jwt from "jsonwebtoken" import { Message } from "./models/Message.js" import { User } from "./models/User.js" import { authenticateUser } from "./middleware/auth.js" -import "./config/db.js" +import { loginLimiter } from "./middleware/loginLimiter.js" // Importerade loginLimiter-middleware för att skydda inloggningsendpointen mot brute-force attacker. +import { connectDB } from "./config/db.js" // Importerar funktionen för att starta databasen stabilt import listEndpoints from "express-list-endpoints" import { body, param, validationResult } from "express-validator" // Importerade express-validator för ökad validering av indata -import rateLimit from "express-rate-limit" // Importerade express-rate-limit för att skydda inloggningsendpointen mot brute-force attacker - if (!process.env.JWT_SECRET) throw new Error("JWT_SECRET is not set in .env") @@ -222,6 +221,8 @@ return res.status(400).json({ success: false, error: "Invalid database ID", erro } }) +await connectDB() + app.listen(PORT, () => { console.log(`Listening on port ${PORT}`) -}) +}) \ No newline at end of file From 1511d83914b329c6d47542e9f126690c2cdea6c0 Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Mon, 8 Jun 2026 09:45:24 +0200 Subject: [PATCH 40/48] =?UTF-8?q?=C3=85tg=C3=A4rdade=20fel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config/db.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/backend/config/db.js b/backend/config/db.js index a4257e74..617714cb 100644 --- a/backend/config/db.js +++ b/backend/config/db.js @@ -1,13 +1,12 @@ import mongoose from "mongoose" -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/messages" - -mongoose.connect(mongoUrl) - -mongoose.connection.once("open", () => { - console.log("Connected to MongoDB") -}) - -mongoose.connection.on("error", err => { - console.error("connection error:", err) -}) +export const connectDB = async () => { + try { + const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/messages" + await mongoose.connect(mongoUrl) + console.log("Connected to MongoDB") + } catch (err) { + console.error("connection error:", err) + process.exit(1) + } +} \ No newline at end of file From 2feb7ff9209f64dfdca196a344fd25b7f9ac1815 Mon Sep 17 00:00:00 2001 From: Sandra Ahlqvist Date: Mon, 8 Jun 2026 10:02:09 +0200 Subject: [PATCH 41/48] =?UTF-8?q?=C3=84ndrat=20felaktighet=20i=20app.jsc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.jsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 445a402f..3d8bbaf3 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -80,16 +80,15 @@ export const App = () => { onClick={() => setModal("register")} className="auth-button" > - Register - - - )} + Register + + + )} {modal && ( setModal(null)} onSuccess={(data) => { - console.log("User logged in:", data) setUser(data) setModal(null) }} From 39925d3ff1ccfc189e5a97dd8655fbffac0323fe Mon Sep 17 00:00:00 2001 From: linnhass Date: Tue, 9 Jun 2026 13:29:34 +0200 Subject: [PATCH 42/48] =?UTF-8?q?Rensade=20README.md=20och=20tog=20bort=20?= =?UTF-8?q?=C3=B6verfl=C3=B6dig=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 59 +------------------------------------------------------ 1 file changed, 1 insertion(+), 58 deletions(-) diff --git a/README.md b/README.md index d5152465..8af38664 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,2 @@ # yh-message-app-fullstack - -Sandra testar. Och testar igen, tjohoo. -Christine lägger in en rad.. testing testing - -DUBBELKOLLA -Inloggningsformulär tillåter enbart text och har maxantal tecken - DUBBELKOLLA KODEN VIA AI FÖR ATT SE ATT DETTA FINNS PÅ PLATS - -Sanitering av all inskickad text via exempelvis ett ORM, Object-Relational Mapper. - DUBBELKOLLA KODEN VIA AI FÖR ATT SE ATT DETTA FINNS PÅ PLATS - -- Anv ska ej kunna radera/ändra andras mess ?? -- Meddelanden ska ej kunna hanteras utan inlogg ?? DUBBELKOLLA KODEN VIA AI FÖR ATT SE OM DETTA FINNS - -Salting + Hashing - KOLLA HUR MAN GÖR OCH VAR? - -Samtliga händelser loggas, när och varifrån - KOLLA MED AI HUR OCH VAR ---------------------------------- -KODNING/AKTIVERING -- Vi ändrar i .env-filen - JWT_SECRET - NEJ!!!! -- Minimikrav för lösenord, 10 tecken, 1 siffra, 1 specialtecken - SERVER.JS - FINNS I JS -- Implementera Session Timeout, 60 min - SERVER.JS - FINNS I JS (rad 28 + 76) -- Begränsa antal försök att logga in (5 försök, sedan paus på 15 min) - SERVER.JS (finns ej) -- Max 3 antal inloggade enheter samtidigt - godkännande via annan enhet vid ny inloggning - SERVER.JS -- Revision av dependencies - NPM AUDIT - kommando - i package.json - Github Dependent Bot (funktion) -- ta bort undantaget för env.example i .gitignore - --------------------------------------------- - -RAD 168 - SAKNAR AUTHENTICATEUSER - ÄNDRA TILL -import jwt from "jsonwebtoken" -import { User } from "../models/User.js" - -export const authenticateUser = async (req, res, next) => { - const token = req.headers.authorization?.replace("Bearer ", "") - if (!token) { - return res.status(401).json({ success: false, message: "No token provided" }) - } - try { - const decoded = jwt.verify(token, process.env.JWT_SECRET) - const user = await User.findById(decoded.userId) - if (!user) { - return res.status(401).json({ success: false, message: "User not found" }) - } - req.user = user - next() - } catch (err) { - res.status(401).json({ success: false, message: "Invalid token" }) - } -} - - ------------------------------------- -BOCKA AV: -- Användaren ska skapa meddelanden -- Anv ska inloggat kunna ta bort och redigera egna mess ------------------------------------- -REKOMMENDATIONER: -- Rutiner för patching, revision av dependencies och back-uper. - +Test test From f6e97b9650c8e186ae15326a843d5e4146f34277 Mon Sep 17 00:00:00 2001 From: linnhass Date: Tue, 9 Jun 2026 13:45:04 +0200 Subject: [PATCH 43/48] =?UTF-8?q?Lagt=20till=20validering=20av=20indata=20?= =?UTF-8?q?f=C3=B6r=20register=20endpoint=20med=20Express-validator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/server.js b/backend/server.js index cb0d8e0f..d6687d01 100644 --- a/backend/server.js +++ b/backend/server.js @@ -28,7 +28,7 @@ app.get("/", (req, res) => { res.send(listEndpoints(app)) }) -app.post("/register", [ +app.post("/register", loginLimiter, [ // Validering av indata för register endpoint adderad via Express-validator body("username").isLength({ min: 6, max: 25 }).withMessage("Username must be between 6 and 25 characters"), // Adderade validering för användarnamn body("email").isEmail().normalizeEmail(), // Validerar att email är i korrekt format och normaliserar det body("password").isLength({ min: 10, max: 64 }).withMessage("Password must be between 10 and 64 characters") // Adderade validering för lösenordslängd From e3590ab3075a5ab9c59032270d98be253de37824 Mon Sep 17 00:00:00 2001 From: linnhass Date: Tue, 9 Jun 2026 14:04:07 +0200 Subject: [PATCH 44/48] =?UTF-8?q?Lagt=20till=20validering=20av=20indata=20?= =?UTF-8?q?f=C3=B6r=20register=20och=20login=20endpoints=20samt=20implemen?= =?UTF-8?q?terat=20rate=20limiter=20f=C3=B6r=20att=20skydda=20mot=20brute-?= =?UTF-8?q?force=20attacker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/server.js b/backend/server.js index d6687d01..3a8b261d 100644 --- a/backend/server.js +++ b/backend/server.js @@ -28,7 +28,7 @@ app.get("/", (req, res) => { res.send(listEndpoints(app)) }) -app.post("/register", loginLimiter, [ // Validering av indata för register endpoint adderad via Express-validator +app.post("/register", rateLimiter, [ // Validering av indata för register endpoint adderad via Express-validator body("username").isLength({ min: 6, max: 25 }).withMessage("Username must be between 6 and 25 characters"), // Adderade validering för användarnamn body("email").isEmail().normalizeEmail(), // Validerar att email är i korrekt format och normaliserar det body("password").isLength({ min: 10, max: 64 }).withMessage("Password must be between 10 and 64 characters") // Adderade validering för lösenordslängd @@ -139,7 +139,7 @@ app.post("/login", loginLimiter, [ // Validering av indata för login adderad vi -app.get("/messages", authenticateUser, async (req, res) => { //Lade till authenticateUser för att säkerställa att endast inloggade användare kan hämta meddelanden. +app.get("/messages", authenticateUser, rateLimiter, async (req, res) => { //Lade till authenticateUser för att säkerställa att endast inloggade användare kan hämta meddelanden. // Lade till rateLimiter för att skydda endpointen mot brute-force attacker. try { const messages = await Message.find() .sort({ createdAt: "desc" }) @@ -153,7 +153,7 @@ app.get("/messages", authenticateUser, async (req, res) => { //Lade till authent }) app.post("/messages", [ - authenticateUser, + authenticateUser, rateLimiter, // lade till rateLimiter för att skydda endpointen mot brute-force attacker. body("message").trim().isLength({ min: 1, max: 250 }).withMessage("Message must be between 1 and 250 characters") ], async (req, res) => { const errors = validationResult(req) From 45f158fbc03f8b2057cc1a8e951e60d709e8df40 Mon Sep 17 00:00:00 2001 From: linnhass Date: Tue, 9 Jun 2026 14:13:01 +0200 Subject: [PATCH 45/48] =?UTF-8?q?Lagt=20till=20global=20rate=20limiter=20f?= =?UTF-8?q?=C3=B6r=20att=20skydda=20alla=20endpoints=20mot=20=C3=B6verbela?= =?UTF-8?q?stningsattacker=20(DoS)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/middleware/rateLimiter.js | 7 +++++++ backend/server.js | 9 ++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 backend/middleware/rateLimiter.js diff --git a/backend/middleware/rateLimiter.js b/backend/middleware/rateLimiter.js new file mode 100644 index 00000000..33b26db3 --- /dev/null +++ b/backend/middleware/rateLimiter.js @@ -0,0 +1,7 @@ +import rateLimit from "express-rate-limit" + +export const globalLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, + message: "Too many requests from this IP, please try again later." +}) diff --git a/backend/server.js b/backend/server.js index 3a8b261d..61bf7b66 100644 --- a/backend/server.js +++ b/backend/server.js @@ -12,6 +12,7 @@ import { loginLimiter } from "./middleware/loginLimiter.js" // Importerade login import { connectDB } from "./config/db.js" // Importerar funktionen för att starta databasen stabilt import listEndpoints from "express-list-endpoints" import { body, param, validationResult } from "express-validator" // Importerade express-validator för ökad validering av indata +import { globalLimiter } from "./middleware/rateLimiter.js" // Global limiter som skyddar alla endpoints mot överbelastningsattacker (DoS) och fixar CodeQL-varningarna if (!process.env.JWT_SECRET) throw new Error("JWT_SECRET is not set in .env") @@ -24,11 +25,13 @@ app.use(cors({ app.use(express.json()) // Vi föreslår att säkra session-tokeln genom att använda HttpOnly cookies istället för att skicka den i JSON-responsen, vilket minskar risken för XSS-attacker. Det här innebär större ändringar i övrig kod för att hantera autentisering och session, vi har valt att inte genomföra detta själva. +app.use(globalLimiter) // Använder globalLimiter som middleware för att skydda alla endpoints mot överbelastningsattacker (DoS) och fixar CodeQL-varningarna + app.get("/", (req, res) => { res.send(listEndpoints(app)) }) -app.post("/register", rateLimiter, [ // Validering av indata för register endpoint adderad via Express-validator +app.post("/register", [ body("username").isLength({ min: 6, max: 25 }).withMessage("Username must be between 6 and 25 characters"), // Adderade validering för användarnamn body("email").isEmail().normalizeEmail(), // Validerar att email är i korrekt format och normaliserar det body("password").isLength({ min: 10, max: 64 }).withMessage("Password must be between 10 and 64 characters") // Adderade validering för lösenordslängd @@ -139,7 +142,7 @@ app.post("/login", loginLimiter, [ // Validering av indata för login adderad vi -app.get("/messages", authenticateUser, rateLimiter, async (req, res) => { //Lade till authenticateUser för att säkerställa att endast inloggade användare kan hämta meddelanden. // Lade till rateLimiter för att skydda endpointen mot brute-force attacker. +app.get("/messages", authenticateUser, async (req, res) => { //Lade till authenticateUser för att säkerställa att endast inloggade användare kan hämta meddelanden. try { const messages = await Message.find() .sort({ createdAt: "desc" }) @@ -153,7 +156,7 @@ app.get("/messages", authenticateUser, rateLimiter, async (req, res) => { //Lade }) app.post("/messages", [ - authenticateUser, rateLimiter, // lade till rateLimiter för att skydda endpointen mot brute-force attacker. + authenticateUser, body("message").trim().isLength({ min: 1, max: 250 }).withMessage("Message must be between 1 and 250 characters") ], async (req, res) => { const errors = validationResult(req) From 8c1c73d5dcf82ab8502ca5c680ad34e304005569 Mon Sep 17 00:00:00 2001 From: linnhass Date: Tue, 9 Jun 2026 14:15:02 +0200 Subject: [PATCH 46/48] =?UTF-8?q?Lagt=20till=20validering=20av=20indata=20?= =?UTF-8?q?f=C3=B6r=20register=20endpoint=20med=20Express-validator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/server.js b/backend/server.js index 61bf7b66..015a695c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -31,7 +31,7 @@ app.get("/", (req, res) => { res.send(listEndpoints(app)) }) -app.post("/register", [ +app.post("/register", loginLimiter, [ // Validering av indata för register adderad via Express-validator body("username").isLength({ min: 6, max: 25 }).withMessage("Username must be between 6 and 25 characters"), // Adderade validering för användarnamn body("email").isEmail().normalizeEmail(), // Validerar att email är i korrekt format och normaliserar det body("password").isLength({ min: 10, max: 64 }).withMessage("Password must be between 10 and 64 characters") // Adderade validering för lösenordslängd From cd55d37c1b0326632131c7c0a704c61ddc4b7a3b Mon Sep 17 00:00:00 2001 From: linnhass Date: Tue, 9 Jun 2026 14:24:42 +0200 Subject: [PATCH 47/48] =?UTF-8?q?Lagt=20till=20anv=C3=A4ndarsparande=20i?= =?UTF-8?q?=20databasen=20vid=20registrering=20f=C3=B6r=20att=20s=C3=A4ker?= =?UTF-8?q?st=C3=A4lla=20att=20token=20endast=20genereras=20f=C3=B6r=20ska?= =?UTF-8?q?pade=20anv=C3=A4ndare?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/server.js b/backend/server.js index 015a695c..e927e690 100644 --- a/backend/server.js +++ b/backend/server.js @@ -58,6 +58,7 @@ app.post("/register", loginLimiter, [ // Validering av indata för register adde const hashedPassword = await bcrypt.hash(password, 10) const user = new User({ username: username.trim(), email, password: hashedPassword }) + await user.save() // Säkerställer att användaren sparas i databasen innan vi genererar token och skickar svar, vilket minskar risken för att en token genereras för en användare som inte faktiskt skapats på grund av ett databasfel eller liknande problem. const accessToken = jwt.sign( { userId: user._id, username: user.username }, From 223a8ca9d011249569bad3b7ec01a80c7416a137 Mon Sep 17 00:00:00 2001 From: linnhass Date: Sun, 14 Jun 2026 18:23:20 +0200 Subject: [PATCH 48/48] Sammanfattning av fas 3 --- granskningsfasen.md | 50 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/granskningsfasen.md b/granskningsfasen.md index 088f9718..11b2903e 100644 --- a/granskningsfasen.md +++ b/granskningsfasen.md @@ -1 +1,49 @@ -# Inlämning 3 - Granskningsfasen \ No newline at end of file +# Inlämning 3 - Granskningsfasen + +Presentation på figma: +https://www.figma.com/board/UitaJT4H8hl28KNGnIGbRr/Projekt-%22s%C3%A4ker-systemutveckling%22?node-id=0-1&t=TnLGOxraNQkjn304-1 + +Vi genomförde säkerhetsscanning med CodeQL och Dependabot via GitHub. De två verktygen identifierade kritiska sårbarheter inom två huvudområden: applikationslogik (brute force och överbelastning) samt externa beroenden (sårbarheter i tredjepartsbibliotek). + +Vi kommer här redovisa för vad dessa sårbarheter skulle innebära, såväl som potentiella konsekvenser och resonemanget bakom våra implementerade åtgärder. + +CodeQL: Problemformulering och risker +När vi körde CodeQL hittade vi problemet att vår app inte hade några begränsningar för hur många gånger någon kunde skicka anrop till servern. Det kallas rate limiting och utan det var vi sårbara för två typer av attacker: +- Brute force-attacker +- Denial of Service (DoS) +Hur vi resonerade kring åtgärder: +För att skydda oss mot dessa hot valde vi att bygga upp ett skydd i flera lager via vår middleware-fil (ratelimiter.js) Tanken är att om ett lager kringgås finns det alltid ett till. + +Vi åtgärdade med ett Globalt skydd (globalLimiter): Vi satte ett tak för hur många anrop en och samma IP-adress får göra, oavsett vilken del av appen de försöker nå. Det skyddar hela systemet mot DoS-attacker. +Bristen vi åtgärdade återfinns i två av OWASP:s mest välkända säkerhetskategorier: +OWASP: A04:2021-Insecure Design, eftersom avsaknad av rate limiting är ett designbeslut som öppnar upp för attacker +OWASP: A05:2021-Security Misconfiguration, eftersom en server utan trafikbegränsningar är en vanlig och allvarlig säkerhetsbrist + +Sen har vi riktat skydd (loginLimiter): Inloggning och registrering är extra känsligt, där behövs ett striktare skydd. Vi la därför till loginLimiter specifikt på dessa ställen. Efter ett visst antal misslyckade försök blockeras IP-adressen automatiskt, vilket gör att brute force-attacker i princip omöjliga eller i alla fall avsevärt försvårade. +Kopplingen till OWASP: +A04:2021 – Insecure Design, eftersom avsaknad av riktad begränsning på känsliga endpoints möjliggör brute force-attacker +A05:2021 – Security Misconfiguration, eftersom autentiseringsendpoints alltid bör ha striktare säkerhetsinställningar. + +Sen till fel ordning i koden (user.save()): Vi hittade också ett problem i hur registreringen fungerade – koden skapade en inloggningstoken innan vi var säkra på att användaren faktiskt sparas i databasen. Det betyder att någon i teorin kunde ha en giltig token för ett konto som inte existerar. Vi fixade detta genom att lägga till await user save() så att databasen alltid sparar användaren först. +Kopplingen till OWASP: +A07:2021 – Identification and Authentication Failures, eftersom fel ordning i autentiseringsflödet kan leda till ogiltiga sessioner och komprometterad identitetshantering. + +Dependabot-fynd: Problemformulering och risker +Via vår scan med Dependabot identifierade vi 16 sårbarheter i projektets dependencies. Det visar på ett tydligt sätt vilka risker som finns med externa bibliotek, där sårbarheter “ärvs” in i egna system. Nedan följer de instanser där sårbarheter hittades: +- tar (via bcrypt): Sårbarheter i arkivhanteringsbibliotek som tar handlar ofta om filöverskrivning (Directory Traversal) eller Denial of Service vid parsning av skadliga filer. Eftersom bcrypt använder detta sub-bibliotek kan applikationen drabbas indirekt vid hantering av krypteringsresurser. +- jsonwebtoken: Äldre versioner av detta bibliotek har kända brister gällande verifiering av signaturer (till exempel att acceptera algoritmen "none"), vilket tillåter angripare att förfalska tokens och kringgå autentiseringen helt. +- Vite, esbuild och qs: Sårbarheter i byggverktyg (Vite, esbuild) kan leda till kodexekvering på utvecklares maskiner eller i CI/CD-pipelines. Brist i qs kan leda till prototyp-förorening (Prototype Pollution), vilket kan krascha applikationen eller manipulera objektstrukturer i JavaScript. + +Resonemang kring åtgärder +OBS! Vi har valt att inte själva implementera dessa patchningar, då vi inte besitter tillräcklig kunskap för att hantera potentiella konflikter eller krascher som kan uppstå vid en sådan åtgärd. Därför ombeds IT-avdelningen att äga ansvaret över samtlig patchning. + +Vi rekommenderar att uppdatera samtliga dependencies till patchade versioner som den mest effektiva strategin. Att ligga kvar på utdaterade versioner innebär att vi accepterar kända sårbarheter (CVE-id) som ligger öppet tillgängliga i publika databaser, vilket gör dem enkla att utnyttja för exempelvis automatiserade attackverktyg. + +Uppdateringen av tar till 7.5.11 och jsonwebtoken till 9.0.0 skulle stänga kända säkerhetshål direkt vid källan. +Genom att uppdatera utvecklingsverktyg (Vite, esbuild) skulle vi även säkra leveranskedjan (Supply Chain Security), vilket kan förhindra att skadlig kod injiceras under kompileringsfasen. +OWASP: A06:2021-Vulnerable and Outdated Components + +Sammanfattning av verksamhetskonsekvenser +Att ignorera de sårbarheter som identifierades hade kunnat leda till obehörig datatillgång, stulna användarkonton och kraschande system. Genom att kombinera ovan kodändringar (CodeQL-åtgärder) med rekommenderad regelbunden patchhantering (Dependabot-åtgärder) skulle vi uppnå en högre säkerhetsnivå (Defense in Depth). Systemet skulle därmed vara både mer motståndskraftigt mot riktade externa attacker och bättre skyddat mot sårbarheter i den egna leveranskedjan. + +Utifrån våra förutsättningar har vi i fas 1 satt upp säkerhetskrav. Vi har åtgärdat en del av dessa i koden och hänvisat till rekommenderade åtgärder som vi ansett varit för komplexa för oss. Vi har även använt oss av verktyg för att identifiera sårbarheter som är ett bra komplement. Vi anser trots dessa åtgärder att vi inte kan vara helt säkra på applikationens säkerhet och rekommenderar att utvecklare granskar koden och genomför regelbunden utvärdering av applikationens säkerhetsposition.