Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
93c0a1f
Started with Adding-component
julialindstrand Feb 17, 2026
3cb9a5c
Adds card and cardlist
julialindstrand Feb 20, 2026
0879ec3
Adds the name after login
julialindstrand Feb 25, 2026
f8ff274
Tries to add comments
julialindstrand Mar 1, 2026
d7bad30
Adds delete
julialindstrand Mar 4, 2026
8006655
Adds delete comment
julialindstrand Mar 4, 2026
39bf200
Adds edit mode
julialindstrand Mar 4, 2026
a0dd1c0
Adds styling
julialindstrand Mar 5, 2026
184b169
adds responsiveness
julialindstrand Mar 5, 2026
0fb7574
A little styling
julialindstrand Mar 5, 2026
5f6ca5d
Adds cats successfully
julialindstrand Mar 5, 2026
332183d
Adds filters
julialindstrand Mar 5, 2026
6ec43c1
Adds remove background
julialindstrand Mar 5, 2026
2fea245
Makes login work *again*
julialindstrand Mar 7, 2026
3cb6324
Adds levels off users
julialindstrand Mar 10, 2026
37a9d42
Now admin can create new admins
julialindstrand Mar 11, 2026
c4482be
Adds the final responsiveness
julialindstrand Mar 11, 2026
9044bac
pin cloudinary to 1.x to satisfy multer-storage-cloudinary peer depen…
julialindstrand Mar 11, 2026
70ec8a6
Changes cloudinary for deployment
julialindstrand Mar 11, 2026
7543d2a
Fixing deployment
julialindstrand Mar 11, 2026
a9ca8c2
More troubleshooting deployment
julialindstrand Mar 11, 2026
5e6c058
More deploytrouble
julialindstrand Mar 11, 2026
7783d0a
Add jsonwebtoken dependency
julialindstrand Mar 11, 2026
8145466
Fix adminname
julialindstrand Mar 11, 2026
d0c0200
Adds renderlink hardcoded
julialindstrand Mar 11, 2026
d5e3528
Adds right render
julialindstrand Mar 11, 2026
fa70457
Fixes spellingerror
julialindstrand Mar 11, 2026
0253f69
Fixed another spellingerror
julialindstrand Mar 11, 2026
bb9a448
Moved the imports
julialindstrand Mar 11, 2026
3a73a92
Removes one handler
julialindstrand Mar 11, 2026
498420c
Added useContext to app
julialindstrand Mar 11, 2026
c79e363
Adds fetchJson to auth
julialindstrand Mar 11, 2026
4939b30
Changes url in auth
julialindstrand Mar 11, 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
5 changes: 5 additions & 0 deletions backend/backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"cloudinary": "^1.41.3"
}
}
18 changes: 18 additions & 0 deletions backend/middleware/authenticate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import jwt from "jsonwebtoken"

export default function authenticate(req, res, next) {
const authHeader = req.headers.authorization

if (!authHeader?.startsWith('Bearer '))
return res.status(401).json({ msg: 'Missing token' })


const token = authHeader.split(' ')[1]
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET)
req.user = decoded
next()
} catch (err) {
return res.status(401).json({ msg: 'Invalid token' })
}
}
11 changes: 11 additions & 0 deletions backend/middleware/authorize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function authorize(...allowedRoles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ msg: "Unauthenticated" })
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({ msg: "Insufficient rights" })
}
next()
}
}
28 changes: 28 additions & 0 deletions backend/middleware/verifyToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import jwt from "jsonwebtoken"
import User from "../models/User.js"

export const verifyToken = async (req, res, next) => {
const authHeader = req.headers.authorization

if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ message: "No token, authorization denied" })
}

const token = authHeader.split(" ")[1]

try {
const decoded = jwt.verify(token, process.env.JWT_SECRET)

const user = await User.findById(decoded.sub)
if (!user) throw new Error("User not found")

req.user = {
_id: user._id,
name: user.name,
role: user.role,
}
next()
} catch (err) {
return res.status(401).json({ message: `Invalid token: ${err.message}` })
}
}
17 changes: 17 additions & 0 deletions backend/models/Cat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import mongoose from "mongoose"
import commentSchema from "./Comments.js"

const catSchema = new mongoose.Schema(
{
name: { type: String, required: true },
gender: {
type: String,
enum: ["male", "female"],
required: true,
},
imageUrl: { type: String, required: true },
location: { type: String, required: true },
comments: [commentSchema], default: [],
}, { timestamps: true })

export default mongoose.models.Cat || mongoose.model("Cat", catSchema)
23 changes: 23 additions & 0 deletions backend/models/Comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import mongoose from "mongoose"

const commentSchema = new mongoose.Schema(
{
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
userName: {
type: String,
required: true,
},
text: {
type: String,
required: true,
minlength: 1,
},
},
{ timestamps: true }
)

export default commentSchema
12 changes: 12 additions & 0 deletions backend/models/User.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import mongoose from "mongoose"

const UserSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ["admin", "user"], default: "user" },
}, { timestamps: true })

const User = mongoose.models.User || mongoose.model("User", UserSchema)

export default User
12 changes: 9 additions & 3 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@
"@babel/core": "^7.17.9",
"@babel/node": "^7.16.8",
"@babel/preset-env": "^7.16.11",
"bcryptjs": "^3.0.3",
"cloudinary": "^1.21.0",
"cors": "^2.8.5",
"express": "^4.17.3",
"mongoose": "^8.4.0",
"dotenv": "^17.3.1",
"express": "^4.22.1",
"jsonwebtoken": "^9.0.3",
"mongoose": "^8.23.0",
"multer": "^2.0.2",
"multer-storage-cloudinary": "^4.0.0",
"nodemon": "^3.0.1"
}
}
}
10 changes: 10 additions & 0 deletions backend/parseBoolean.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const parseBoolean = (value) => {
if (typeof value === "boolean") return value
if (typeof value === "string") {
const lowered = value.toLowerCase().trim()
if (["true", "1", "yes", "y"].includes(lowered)) return true
if (["false", "0", "no", "n"].includes(lowered)) return false
}

return false
}
86 changes: 86 additions & 0 deletions backend/routes/adminRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import express from "express"
import authenticate from "../middleware/authenticate.js"
import authorize from "../middleware/authorize.js"
import User from "../models/User.js"
import bcrypt from "bcryptjs"

const adminRouter = express.Router()

adminRouter.get(
"/",
authenticate,
authorize("admin"),
async (req, res) => {
try {
const stats = {
totalUsers: await User.countDocuments(),
totalAdmins: await User.countDocuments({ role: "admin" }),
}
res.status(200).json({ success: true, data: stats })
} catch (error) {
console.error("Admin stats error:", error)
res.status(500).json({ success: false, message: error.message })
}
}
)

// Create User
adminRouter.post(
"/users",
authenticate,
authorize("admin"),
async (req, res) => {
try {
const { name, email, role, password } = req.body

// Validate required fields
if (!name || !email || !password) {
return res.status(400).json({
success: false,
message: "Name, email, and password are required"
})
}

// Check if user already exists
const existingUser = await User.findOne({ email })
if (existingUser) {
return res.status(400).json({
success: false,
message: "User with this email already exists"
})
}

// Hash password
const hashedPassword = await bcrypt.hash(password, 10)

// Create new user
const newUser = new User({
name,
email,
role: role || "user",
password: hashedPassword
})

await newUser.save()

res.status(201).json({
success: true,
message: "User created successfully",
data: {
id: newUser._id,
name: newUser.name,
email: newUser.email,
role: newUser.role
}
})
} catch (error) {
console.error("User creation error:", error)
res.status(500).json({
success: false,
message: error.message
})
}
}
)

export default adminRouter
142 changes: 142 additions & 0 deletions backend/routes/catRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import express, { } from "express"
import multer from "multer"
import dotenv from "dotenv"
import { CloudinaryStorage } from "multer-storage-cloudinary"
import cloudinary from "cloudinary"
import Cat from "../models/Cat"
import authenticate from "../middleware/authenticate"
import authorize from "../middleware/authorize"


dotenv.config()

// Cloudinary
const { v2: cloudinaryV2 } = cloudinary
cloudinaryV2.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
})

const storage = new CloudinaryStorage({
cloudinary: cloudinaryV2,
params: {
folder: "cats",
allowedFormats: ["jpg", "png"],
transformation: [{
width: 500,
height: 500,
crop: "limit",
effect: "background_removal:fineedges"
}],
},
})

const parser = multer({ storage })

const router = express.Router()

// All cats
router.get("/cats", async (req, res) => {
try {
const cats = await Cat.find().sort({ createdAt: -1 })
res.json(cats)

} catch (error) {
res.status(500).json({ error: "Failed to fetch cats" })
}
})

// One cat
router.get("/cats/:id", async (req, res) => {
const id = req.params.id
try {
const cat = await Cat.findById(id)
res.json(cat)

} catch (error) {
res.status(500).json({ error: "Failed to fetch cat" })
}
})

// Post
router.post("/cats", authenticate, authorize('admin', 'editor'), parser.single('picture'), async (req, res) => {
try {

const { filename, gender, location } = req.body

if (!req.file) {
return res.status(400).json({ message: "Image file missing" })
}
if (!filename || !gender || !location) {
return res.status(400).json({ message: "All fields are required" })
}

const cat = await new Cat({
name: filename,
imageUrl: req.file.path,
gender,
location,
}).save()

res.status(201).json(cat)
} catch (err) {
console.error('Save error:', err)
if (err.name === 'ValidationError') {
return res.status(400).json({ message: err.message, errors: err.errors })
}
res.status(500).json({ message: err.message || 'Server error' })
}
})

// Edit
router.put("/cats/:id", authenticate, authorize('admin', 'editor'), async (req, res) => {
try {

const editedCat = req.body

const cat = await Cat.findById(editedCat._id)

cat.name = editedCat.name
cat.gender = editedCat.gender
cat.location = editedCat.location

await cat.save()
res.json(cat)

} catch (error) {
res.status(500).json({ error: "Failed to edit cat" })
}
})

// Delete
router.delete("/cats/:id", authenticate, authorize('admin', 'editor'), async (req, res) => {
const id = req.params.id
try {
const cat = await Cat.findById(id)

if (!cat) {
return res.status(404).json({
success: false,
response: [],
message: "Cat not found"
})
}

await Cat.findByIdAndDelete(id)

res.status(200).json({
success: true,
response: id,
message: "Cat deleted successfully"
})
} catch (error) {
res.status(500).json({
success: false,
response: null,
message: error,
})
}
})

export default router
Loading