Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
97c1a28
Sandra testar
haxxmonkey-ship-it May 27, 2026
e42f394
Sandra test 2
haxxmonkey-ship-it May 27, 2026
50910b0
Inlämning av planering
haxxmonkey-ship-it May 27, 2026
beb423b
Add screenshot to planeringsfasen.md
haxxmonkey-ship-it May 27, 2026
2ce1381
Tog bort onödig text
haxxmonkey-ship-it May 27, 2026
b07af41
testtest
haxxmonkey-ship-it May 27, 2026
aefc208
Merge branch 'main' of https://github.com/haxxmonkey-ship-it/yh-messa…
haxxmonkey-ship-it May 27, 2026
cbbe2ac
Update README with additional test message
christinevaren1984-crypto May 27, 2026
c712b15
Anteckningar
haxxmonkey-ship-it May 29, 2026
08ebd26
Anteckningar
haxxmonkey-ship-it May 29, 2026
2c6c636
Anteckningar
haxxmonkey-ship-it May 29, 2026
8fae300
Småändringar
haxxmonkey-ship-it Jun 1, 2026
30f61ad
Småändringar
haxxmonkey-ship-it Jun 1, 2026
81957b7
Merge branch 'main' of https://github.com/haxxmonkey-ship-it/yh-messa…
haxxmonkey-ship-it Jun 1, 2026
a7f672d
Express Validator installation + revision
haxxmonkey-ship-it Jun 1, 2026
9c77292
Save 2
haxxmonkey-ship-it Jun 1, 2026
b94a980
Inputverifiering
haxxmonkey-ship-it Jun 1, 2026
870fa85
Adderat isOwner
linnhass Jun 2, 2026
f2a0f27
föreslagit ändring av sesion-tokens
linnhass Jun 2, 2026
e46b13b
kommentar om rekomendation om att göra meddelanden osynliga för ej in…
linnhass Jun 2, 2026
d7db263
ENV Create
haxxmonkey-ship-it Jun 3, 2026
7661789
Adjusted logged off message view
haxxmonkey-ship-it Jun 3, 2026
89f7f9c
Frontend adjustment
haxxmonkey-ship-it Jun 3, 2026
3054b46
Adderat loginLimiter för att begränsa antal försökt till inloggning
linnhass Jun 3, 2026
db2c9e8
Adderade Express-Validator (param)
haxxmonkey-ship-it Jun 3, 2026
89bf579
Merge branch 'main' of https://github.com/haxxmonkey-ship-it/yh-messa…
haxxmonkey-ship-it Jun 3, 2026
09eefb2
Adderade loginLimiter
haxxmonkey-ship-it Jun 3, 2026
c666cb9
Åtgärdar felkoder i server.js
haxxmonkey-ship-it Jun 5, 2026
fc5779f
Edit
haxxmonkey-ship-it Jun 5, 2026
e8da759
Validering i inputfält login
linnhass Jun 5, 2026
5a5fef4
ändrat felmeddelande
linnhass Jun 5, 2026
da341dc
Ändrade måsvinge i singlemessage.jsx
haxxmonkey-ship-it Jun 5, 2026
c0d1b68
Tog bort felaktig kommentar
haxxmonkey-ship-it Jun 5, 2026
3a4ea7e
Justerat siffor
linnhass Jun 5, 2026
230c318
Åtgärdade felaktigheter i authmodal.jsx
haxxmonkey-ship-it Jun 5, 2026
44e4e04
Merge branch 'main' of https://github.com/haxxmonkey-ship-it/yh-messa…
haxxmonkey-ship-it Jun 5, 2026
0e599c6
Löste merge konflikt i server.js
linnhass Jun 5, 2026
06e9a5f
Lagt till parametrar i User.js
linnhass Jun 5, 2026
b34436b
Åtgärdade misstag i koden
haxxmonkey-ship-it Jun 8, 2026
1116a7b
Åtgärndat felaktig merge
linnhass Jun 8, 2026
ac2b2d9
Merge branch 'main' of https://github.com/haxxmonkey-ship-it/yh-messa…
linnhass Jun 8, 2026
7ba17a6
Åtgärdade fel i AuthModal
haxxmonkey-ship-it Jun 8, 2026
3ce526d
lagt till kommentar
linnhass Jun 8, 2026
7b4b087
Åtgärdade fel
haxxmonkey-ship-it Jun 8, 2026
1511d83
Åtgärdade fel
haxxmonkey-ship-it Jun 8, 2026
2feb7ff
Ändrat felaktighet i app.jsc
haxxmonkey-ship-it Jun 8, 2026
39925d3
Rensade README.md och tog bort överflödig text
linnhass Jun 9, 2026
f6e97b9
Lagt till validering av indata för register endpoint med Express-vali…
linnhass Jun 9, 2026
e3590ab
Lagt till validering av indata för register och login endpoints samt …
linnhass Jun 9, 2026
45f158f
Lagt till global rate limiter för att skydda alla endpoints mot överb…
linnhass Jun 9, 2026
8c1c73d
Lagt till validering av indata för register endpoint med Express-vali…
linnhass Jun 9, 2026
cd55d37
Lagt till användarsparande i databasen vid registrering för att säker…
linnhass Jun 9, 2026
223a8ca
Sammanfattning av fas 3
linnhass Jun 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,7 @@ dist
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vite/

# Environment variables file
.env

3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# yh-message-app-fullstack
# yh-message-app-fullstack
Test test
21 changes: 10 additions & 11 deletions backend/config/db.js
Original file line number Diff line number Diff line change
@@ -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)
}
}
10 changes: 10 additions & 0 deletions backend/middleware/loginLimiter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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
max: 5,
message: {
success: false,
message: "För många inloggningsförsök. Försök igen om 15 minuter."
}
});
7 changes: 7 additions & 0 deletions backend/middleware/rateLimiter.js
Original file line number Diff line number Diff line change
@@ -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."
})
5 changes: 4 additions & 1 deletion backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@ const userSchema = new mongoose.Schema({
type: String,
required: true,
unique: true,
minlength: 2,
maxlength: 25, // maxlängd på användarnamn satt till 25 tecken för att undvika överdrivet långa användarnamn
trim: true,
},
email: {
type: String,
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
},
})

Expand Down
29 changes: 29 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
114 changes: 82 additions & 32 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,69 @@
import "dotenv/config"
import helmet from "helmet"
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"
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 { 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")

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.

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", async (req, res) => {
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
], 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 })
await user.save()
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 },
process.env.JWT_SECRET,
{ expiresIn: "2h" }
{ expiresIn: "1h" } // Sänkte token expiration time till 1 timme för ökad säkerhet
)

res.status(201).json({
Expand All @@ -64,16 +75,24 @@ app.post("/register", async (req, res) => {
accessToken,
},
})
} catch (error) {
} 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.
})
}
})

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) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, message: "Validation failed", errors: errors.array() })
}
})

app.post("/login", async (req, res) => {
try {
const { login, password } = req.body
const user = await User.findOne({
Expand All @@ -100,7 +119,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" } //Sänkte token expiration time till 1 timme för ökad säkerhet
)

res.json({
Expand All @@ -113,17 +132,18 @@ app.post("/login", async (req, res) => {
},
})
} 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)

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" })
Expand All @@ -136,23 +156,39 @@ 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: 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(400).json({ message: "Could not save message", errors: err.errors })
}
})

app.patch("/messages/:id", authenticateUser, async (req, res) => {
if (!isValidId(req.params.id)) return res.status(400).json({ error: "Invalid message ID" })
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) 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" })
}

Expand All @@ -165,18 +201,32 @@ app.patch("/messages/:id", authenticateUser, async (req, res) => {
}
})

app.delete("/messages/:id", async (req, res) => {
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 || 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(400).json({ error: "Could not delete message" })
}
})

await connectDB()

app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`)
})
})
4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -24,4 +24,4 @@
"eslint-plugin-react-refresh": "^0.4.3",
"vite": "^4.4.5"
}
}
}
Loading