project-root
├── package.json # Project metadata, dependencies, scripts, etc.
├── .env # Environment variables (local setup)
├── .env.sample # Sample environment variables file for reference
├── server.js # Main server entry point
│
├── config
│ └── dbConnect.js # Database connection setup
│
├── routes
│ └── userRouter.js # Defines user-related routes
│
├── controllers
│ └── userController.js # Handles user-related logic (controller for user actions)
│
├── models
│ └── User.js # User model/schema (database structure and methods)
│
├── middleware
│ ├── validateRequest.js # Middleware for request validation
│ ├── jwt.js # JWT authentication middleware
│ └── roleCheck.js # Role-based access control middleware
│
└── validation
└── userValidation.js # User-specific validation rules
Server.js
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import connectDB from "./config/dbConnect.js";
import userRouter from "./routes/userRouter.js";
connectDB(); // start db connection
const PORT = process.env.PORT;
const app = express();
app.use(cookieParser());
app.use(cors({credentials:true, origin:"http://localhost:5173"}));
app.use(express.json()); // body parser
app.use("/users", userRouter);
// ... more routes when needed
app.listen(PORT, () => {
console.log(`Server is listening on port: ${PORT}`)
});
.env
PORT=3000
MONGODB_URI=mongodb://127.0.0.1:27017
DATABASE=users
config/dbConnect.js
import mongoose from "mongoose";
mongoose.connection.on('error', (error) => {
console.log('DB error after initial connection:', error);
});
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URL, {
dbName: process.env.DATABASE
});
console.log('Connected to MongoDB!');
} catch (error) {
console.error('Connection error:', error);
}
};
export default connectDB;
routes/userRouter.js
import {Router} from "express";
import * as user from "../controllers/userController.js"
import { authenticate } from "../middleware/jwt.js";
import { roleCheck } from "../middleware/roleCheck.js";
const userRouter = Router();
userRouter
.post("/register", user.createUser)
.post("/login", user.userLogin)
.get("/", authenticate, user.dashboard)
.put("/", authenticate, roleCheck(["editor", "admin"]), user.editMessage)
.delete("/", authenticate, roleCheck(["admin"]), user.deleteUser)
export default userRouter;
controllers/userController.js
import { User } from "../models/User.js";
import { generateToken } from "../middleware/jwt.js";
export const createUser = async(req, res) => {
try {
const user = await User.create(req.body);
res.status(201).json({msg: "Neuer Benutzer wurde gespeichert", user});
} catch (error) {
console.log(error);
res.status(500).json({msg:"Server error!"})
}
}
export const userLogin = async(req, res)=>{
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) return res.status(404).json({msg:"Unbekannter Benutzer!"});
const passwordMatch = await user.authenticate(password);
if (!passwordMatch) return res.status(401).json({msg:"Fehlerhafte Anmeldedaten!"})
const token = generateToken({userId:user._id});
return res.status(200).cookie("jwt", token, {httpOnly:true, maxAge: 60*60*1000}).json({msg:"Anmeldung erfolgreich!"});
} catch (error) {
console.log(error);
res.status(500).json({msg:"Server error!"})
};
}
export const dashboard = async(req, res) => {
try {
res.status(200).send(`Hallo ${req.user.name}, willkommen auf unserer Seite!`);
} catch (error) {
console.log(error);
res.status(500).json({msg:"Server error!"})
}
}
models/User.js
import {Schema, model} from "mongoose";
import bcrypt from "bcrypt";
const userSchema = new Schema({
name:{type:String, required:true},
email: { type: String, unique: true, required:true },
password: { type: String, required:true, select:false },
roles:{[
type:String,
enum: ["user", "editor", "admin"]
]}
});
/*
Function is called before a document is saved into the db
with User.create() or user.save()
*/
userSchema.pre("save", async function(next){
try {
if (this.isModified("password")) { // only hash if the password has been modified
const hash = await bcrypt.hash(this.password, 12);
this.password = hash;
}
next();
} catch (error) {
next(error);
}
});
/*
Name after methods can be random, function is called from the
controller, by attaching it to the user instance (not User model):
user.authenticate()
*/
userSchema.methods.authenticate = async function(password){
return await bcrypt.compare(password, this.password);
};
/*
toJSON is a special method, the function is called whenever somewhere
data is converted to json (by res.send or res.json)
*/
userSchema.methods.toJSON = function(){
const user = this.toObject();
delete user.password;
return user;
};
export const User = new model('User', userSchema);
middleware/jwt.js
import jwt from "jsonwebtoken";
import User from "../model/User.js";
export const generateToken = (payload) => {
return jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN || "1h",
});
};
const verifyToken = (token) => {
return jwt.verify(token, process.env.JWT_SECRET);
};
export const authenticate = async (req, res, next) => {
try {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ').at(-1);
// If token is saved in a cookie
// const token = req.cookies.jwt;
if (!token) {
return res.status(401).json({ msg: "Authentification failed!" });
}
const decoded = verifyToken(token);
const user = await User.findById(decoded.userId);
if (!user) {
return res.status(404).json({ msg: "User not found!" });
}
req.user = user;
next();
} catch (error) {
return res.status(403).json({ msg: "Authentification error!" });
}
};
middleware/validateRequest.js
// Middleware to validate JSON against the provided schema
const validateRequest = (validateFn) => (req, res, next) => {
const valid = validateFn(req.body); // Compare body with the compiled function
if (!valid) {
// If errors occured ,they are stored in the errors property of the function
const errors = validateFn.errors.map((err) => err.message);
return res.status(400).json({ errors });
}
next();
};
export default validateRequest;
middleware/roleCheck.js
export const roleCheck = (targetRole) => {
// This is a common pattern in Express middleware
// It's a function that returns another function
// aka. a "higher-order function"
// The inner function is the actual middleware
// It has access to the targetRole variable
return function roleCheckMiddleware(req, res, next) {
const hasRights = targetRole.includes(req.user.role)
// for more than one role (if req.user.roles is an array)
// const hasRights = targetRoles.some(role => req.user.roles.includes(role));
console.log(req.user.role, {hasRights});
if (!hasRights) {
return res.status(403).json({ error: "Nicht autorisiert!" })
}
next()
}
}
validation/userValidation.js
import Ajv from "ajv";
import addFormats from "ajv-formats";
import addErrors from "ajv-errors";
// Initialize ajv
const ajv = new Ajv({ allErrors: true }); // Show all errors, don't stop after the first one
addFormats(ajv); // Add format validators (e.g., email, URL)
addErrors(ajv); // Add support for custom error messages
const userSchema = {
type: "object",
properties: {
name: {
type: "string",
minLength: 1,
errorMessage: "Bitte einen Namen eingeben!",
},
email: {
type: "string",
format: "email",
errorMessage: "Bitte eine gültige E-Mail eingeben!",
},
password: {
type: "string",
minLength: 8,
maxLength: 30,
pattern: "(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])",
errorMessage: {
minLength: "Das Passwort hat nicht die erforderliche Länge (8-30 Zeichen)!",
maxLength: "Das Passwort hat nicht die erforderliche Länge (8-30 Zeichen)!",
pattern:
"Das Passwort muss aus Groß-und Kleinbuchstaben bestehen und mindestens eine Zahl enthalten!",
},
},
website: {
type: ["string", "null"],
pattern: "^$|^(https?://.+)$", // Allow empty string or a valid URL
// format: "uri",
nullable: true, // Allows undefined or null
errorMessage: {
"type": "Die Webseite muss eine Zeichenkette oder null sein!",
"pattern": "Die Webseite muss eine gültige URL enthalten oder leer sein!"
},
},
location: {
type: "string",
enum: ["Deutschland", "Österreich", "Schweiz"],
errorMessage: "Anmeldung nur in der DACH-Region möglich!",
},
blogposts: {
type: "array",
items: {
type: "string",
pattern: "^[0-9a-fA-F]{24}$", // ObjectId validation
errorMessage: "Ungültige ObjectId!",
},
},
},
required: ["name", "email", "password"],
additionalProperties: false,
errorMessage: {
required: {
name: "Bitte einen Namen eingeben!",
email: "Bitte eine gültige E-Mail eingeben!",
password: "Das Passwort ist erforderlich!",
},
},
};
// Compile the schema and return true or false (if errors occur)
const validateUser = ajv.compile(userSchema);
export default validateUser;
seed.js
import { faker } from "@faker-js/faker";
import Participant from "./models/Participant.js";
import Breakoutroom from "./models/Breakoutroom.js";
import connectDB from "./config/dbConnect.js";
connectDB(); // start db connection
const deleteAllRooms = async() => {
console.log("starting to delete");
return await Participant.deleteMany();
};
const deleteAllParticipants = async() => {
return await Breakoutroom.deleteMany();
};
const participants = [];
let counter = 1; // To get all participants into rooms
const createParticipant = async() => {
const newParticipant = new Participant({
name: faker.name.fullName(),
city: faker.address.city(),
email: faker.internet.email(),
});
const result = await newParticipant.save();
participants.push(result["_id"]);
};
const createRoom = async(num) => {
const newRoom = new Breakoutroom({
number: num,
participants: getRandomIds(),
});
const result = await newRoom.save();
};
const createParticipants = async (count = 1) => {
for (let i = 0; i < count; i++) {
await createParticipant();
}
};
const createRooms = async (count) => {
for (let i = 0; i < count; i++) {
await createRoom(i + 1);
}
};
const getRandomIds = () => {
if (participants.length < 1) return [];
let max = participants.length;
if (participants.length > 3) max = Math.abs(max/2);
let number = Math.floor(Math.random()*(max - 1 + 1) + 1);
const selection = [];
if (counter === +process.argv[3]) number = participants.length;
for (let i = 0; i < number; i++) {
selection.push(participants[i]);
}
counter++;
participants.splice(0, number);
return selection;
};
try {
if (!process.argv.includes("doNotDelete")) {
console.log("Deleting all documents...");
await deleteAllRooms();
await deleteAllParticipants();
console.log("finished deleting!");
}
const count1 = process.argv[2] === "doNotDelete" ? undefined : process.argv[2];
const count2 = process.argv[3] === "doNotDelete" ? undefined : process.argv[3];
await createParticipants(count1);
console.log("Participants created!");
await createRooms(count2);
console.log("Rooms created!");
process.exit(0);
} catch (error) {
console.error(error);
process.exit(1);
}