Fork spilum-net-backend
This commit is contained in:
commit
d2ab2dff9d
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
npm-debug.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.env
|
||||||
|
node_modules/
|
||||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD ["node", "src/server.js"]
|
||||||
1796
package-lock.json
generated
Normal file
1796
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "spilum-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "nodemon ./src/server.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
|
"body-parser": "^2.2.2",
|
||||||
|
"cors": "^2.8.6",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"express-rate-limit": "^8.2.1",
|
||||||
|
"helmet": "^8.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"morgan": "^1.10.1",
|
||||||
|
"pg": "^8.19.0",
|
||||||
|
"sequelize": "^6.37.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
|
"nodemon": "^3.1.14"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/config/auth.config.js
Normal file
3
src/config/auth.config.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
secret: process.env.JWT_SECRET
|
||||||
|
};
|
||||||
14
src/config/db.config.js
Normal file
14
src/config/db.config.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
HOST: process.env.PG_HOST,
|
||||||
|
USER: process.env.PG_USER,
|
||||||
|
PASSWORD: process.env.PG_PASS,
|
||||||
|
DB: process.env.PG_DB,
|
||||||
|
PORT: process.env.PG_PORT,
|
||||||
|
dialect: "postgres",
|
||||||
|
pool: {
|
||||||
|
max: 5,
|
||||||
|
min: 0,
|
||||||
|
acquire: 30000,
|
||||||
|
idle: 10000
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/controllers/auth.controller.js
Normal file
102
src/controllers/auth.controller.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
const db = require("../models");
|
||||||
|
const config = require("../config/auth.config");
|
||||||
|
const User = db.user;
|
||||||
|
const Role = db.role;
|
||||||
|
|
||||||
|
const Op = db.Sequelize.Op;
|
||||||
|
|
||||||
|
var jwt = require("jsonwebtoken");
|
||||||
|
var bcrypt = require("bcryptjs");
|
||||||
|
|
||||||
|
// Sign Up user
|
||||||
|
exports.signup = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {username, email, password, roles} = req.body;
|
||||||
|
|
||||||
|
if (!username || !email || !password) {
|
||||||
|
return res.status(400).send({message: "Missing required fields: username, email, password"});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.create({
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password: bcrypt.hashSync(password, 8)
|
||||||
|
})
|
||||||
|
.then(user => {
|
||||||
|
if (roles) {
|
||||||
|
Role.findAll({
|
||||||
|
where: {
|
||||||
|
name: {
|
||||||
|
[Op.or]: req.body.roles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(roles => {
|
||||||
|
user.setRoles(roles).then(() => {
|
||||||
|
res.status(201).send({message: "User was created and roles were added successfully.", user, roles});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
user.setRoles([1]).then(() => {
|
||||||
|
res.status(201).send({message: "User was created successfully.", user})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error("signup error:", err);
|
||||||
|
|
||||||
|
return res.status(500).send({message: err.message || "internal server error."});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.signin = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {username, password} = req.body;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).send({message: "Missing required fields: username, password."});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
where: {
|
||||||
|
username: username
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) return res.status(404).send({message: "User not found."});
|
||||||
|
|
||||||
|
let passwordIsValid = bcrypt.compareSync(
|
||||||
|
password,
|
||||||
|
user.password
|
||||||
|
);
|
||||||
|
if (!passwordIsValid) return res.status(401).send({accessToken: null,message: "Invalid password."});
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{id: user.id},
|
||||||
|
config.secret,
|
||||||
|
{
|
||||||
|
algorithm: "HS256",
|
||||||
|
allowInsecureKeySizes: true,
|
||||||
|
expiresIn: 86400 // 24 hours
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let authorities = [];
|
||||||
|
user.getRoles().then(roles => {
|
||||||
|
for (let i = 0; i < roles.length; i++) {
|
||||||
|
authorities.push("ROLE_" + roles[i].name.toUpperCase());
|
||||||
|
}
|
||||||
|
res.status(200).send({
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
roles: authorities,
|
||||||
|
accessToken: token
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error("signin error:", err);
|
||||||
|
|
||||||
|
res.status(500).send({message: err.message || "Internal server error."});
|
||||||
|
}
|
||||||
|
};
|
||||||
119
src/controllers/bill.controller.js
Normal file
119
src/controllers/bill.controller.js
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
const db = require("../models");
|
||||||
|
const {sequelize, Sequelize} = db;
|
||||||
|
const {literal} = Sequelize;
|
||||||
|
const Bill = db.bill;
|
||||||
|
|
||||||
|
// Get all bills
|
||||||
|
exports.getBills = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const bills = await Bill.findAll();
|
||||||
|
|
||||||
|
return res.status(200).send(bills);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("getBills error:", err);
|
||||||
|
|
||||||
|
return res.status(500).send({message: err.message || "Internal server error."});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all bills by month
|
||||||
|
exports.getBillsByMonth = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const month = parseInt(req.params.month, 10);
|
||||||
|
if (!Number.isInteger(month) || month < 1 || month > 12) {
|
||||||
|
return res.status(400).send({message: "Invalid month parameter. Must be 1-12."});
|
||||||
|
}
|
||||||
|
|
||||||
|
const bills = await Bill.findAll({
|
||||||
|
where: literal(`EXTRACT(MONTH FROM "due") = ${month}`)
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send(bills);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("getBillsByMonth error:", err);
|
||||||
|
|
||||||
|
return res.status(500).send({message: err.message || "Internal server error."});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get one bill by ID
|
||||||
|
exports.getBillById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = req.params.id;
|
||||||
|
const bill = await Bill.findByPk(id);
|
||||||
|
if (!bill) return res.status(404).send({message: "Bill not found."});
|
||||||
|
|
||||||
|
return res.status(200).send(bill);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("getBillsByMonth error:", err);
|
||||||
|
|
||||||
|
return res.status(500).send({message: err.message || "Internal server error."});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create bill
|
||||||
|
exports.createBill = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {name, amount, due, type, shared} = req.body;
|
||||||
|
|
||||||
|
if (!name || amount == null || !due) {
|
||||||
|
return res.status(400).send({message: "Missing required fields: name, amount, due."});
|
||||||
|
}
|
||||||
|
|
||||||
|
const bill = await Bill.create({
|
||||||
|
name,
|
||||||
|
amount,
|
||||||
|
due,
|
||||||
|
type: type || null,
|
||||||
|
shared: !!shared // wtf is this?
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(201).send({message: "Bill created successfully.", bill});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("createBill error:", err);
|
||||||
|
|
||||||
|
return res.status(500).send({message: err.message || "Internal server error."});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete one bill by id
|
||||||
|
exports.deleteBill = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = req.params.id;
|
||||||
|
const deleted = await Bill.destroy({where: {id}});
|
||||||
|
|
||||||
|
if (!deleted) return res.status(404).send({message: "Bill not found."});
|
||||||
|
return res.status(200).send({message: "Bill deleted successfully."});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("deleteBill error:", err);
|
||||||
|
|
||||||
|
return res.status(500).send({message: err.message || "Internal server error."});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update one bill by id
|
||||||
|
exports.updateBill = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = req.params.id;
|
||||||
|
const bill = await Bill.findByPk(id);
|
||||||
|
|
||||||
|
if (!bill) return res.status(404).send({message: "Bill not found."});
|
||||||
|
|
||||||
|
const {name, amount, due, type, shared} = req.body;
|
||||||
|
|
||||||
|
// Only update provided fields
|
||||||
|
if (name !== undefined) bill.name = name;
|
||||||
|
if (amount !== undefined) bill.amount = amount;
|
||||||
|
if (due !== undefined) bill.due = due;
|
||||||
|
if (type !== undefined) bill.type = type;
|
||||||
|
if (shared !== undefined) bill.shared = shared;
|
||||||
|
|
||||||
|
await bill.save();
|
||||||
|
|
||||||
|
return res.status(200).send({message: "Bill updated successfully.", bill});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("updateBill error:", err);
|
||||||
|
|
||||||
|
return res.status(500).send({message: err.message || "Internal server error."});
|
||||||
|
}
|
||||||
|
};
|
||||||
15
src/controllers/user.controller.js
Normal file
15
src/controllers/user.controller.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
exports.allAccess = async (req, res) => {
|
||||||
|
res.status(200).send("Public Content.");
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.userBoard = async (req, res) => {
|
||||||
|
res.status(200).send("User Content.");
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.adminBoard = async (req, res) => {
|
||||||
|
res.status(200).send("Admin Content.");
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.moderatorBoard = async (req, res) => {
|
||||||
|
res.status(200).send("Moderator Content.");
|
||||||
|
};
|
||||||
41
src/middleware/authJwt.js
Normal file
41
src/middleware/authJwt.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
const config = require("../config/auth.config");
|
||||||
|
const db = require("../models");
|
||||||
|
const User = db.user;
|
||||||
|
|
||||||
|
verifyToken = (req, res, next) => {
|
||||||
|
const token = req.headers["x-access-token"] || req.headers.authorization;
|
||||||
|
|
||||||
|
if (!token) return res.status(403).send({message: "No token provided!"});
|
||||||
|
|
||||||
|
const raw = token.startsWith("Bearer ") ? token.slice(7) : token;
|
||||||
|
jwt.verify(raw, config.secret, (err, decoded) => {
|
||||||
|
if (err) return res.status(401).send({message: "Unauthorized!",});
|
||||||
|
req.userId = decoded.id;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const requireRole = (roleNames) => async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (!req.userId) return res.status(401).send({message: "User ID missing in request."});
|
||||||
|
const user = await User.findByPk(req.userId);
|
||||||
|
if (!user) return res.status(404).send({message: "User not found."});
|
||||||
|
|
||||||
|
const roles = await user.getRoles();
|
||||||
|
const has = roles.some(r => roleNames.includes(r.name));
|
||||||
|
if (!has) return res.status(403).send({message: `Require ${roleNames.join(" or ")} Role!`});
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Role check error:", err);
|
||||||
|
res.status(500).send({message: "Internal server error."});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
verifyToken,
|
||||||
|
isAdmin: requireRole(["admin"]),
|
||||||
|
isModerator: requireRole(["moderator"]),
|
||||||
|
isModeratorOrAdmin: requireRole(["moderator", "admin"])
|
||||||
|
};
|
||||||
7
src/middleware/index.js
Normal file
7
src/middleware/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const authJwt = require("./authJwt");
|
||||||
|
const verifySignUp = require("./verifySignUp");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
authJwt,
|
||||||
|
verifySignUp
|
||||||
|
};
|
||||||
57
src/middleware/verifySignUp.js
Normal file
57
src/middleware/verifySignUp.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
const db = require('../models');
|
||||||
|
const ROLES = db.ROLES;
|
||||||
|
const User = db.user;
|
||||||
|
|
||||||
|
checkDuplicateUsernameOrEmail = (req, res, next) => {
|
||||||
|
// Username
|
||||||
|
User.findOne({
|
||||||
|
where: {
|
||||||
|
username: req.body.username
|
||||||
|
}
|
||||||
|
}).then(user => {
|
||||||
|
if (user) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Failed! Username is already in use!"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email
|
||||||
|
User.findOne({
|
||||||
|
where: {
|
||||||
|
email: req.body.email
|
||||||
|
}
|
||||||
|
}).then(user => {
|
||||||
|
if (user) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Failed! Email is already in use!"
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
checkRolesExisted = (req, res, next) => {
|
||||||
|
if (req.body.roles) {
|
||||||
|
for (let i = 0; i < req.body.roles.length; i++) {
|
||||||
|
if (!ROLES.includes(req.body.roles[i])) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Failed! Role does not exist = " + req.body.roles[i]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifySignUp = {
|
||||||
|
checkDuplicateUsernameOrEmail: checkDuplicateUsernameOrEmail,
|
||||||
|
checkRolesExisted: checkRolesExisted
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = verifySignUp;
|
||||||
36
src/models/bill.model.js
Normal file
36
src/models/bill.model.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
module.exports = (sequelize, DataTypes) => {
|
||||||
|
const Bill = sequelize.define("bills", {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
due: {
|
||||||
|
type: DataTypes.DATEONLY,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
shared: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: "bills",
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return Bill;
|
||||||
|
};
|
||||||
40
src/models/index.js
Normal file
40
src/models/index.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
const config = require("../config/db.config");
|
||||||
|
|
||||||
|
const Sequelize = require("sequelize");
|
||||||
|
const sequelize = new Sequelize(
|
||||||
|
config.DB,
|
||||||
|
config.USER,
|
||||||
|
config.PASSWORD,
|
||||||
|
{
|
||||||
|
host: config.HOST,
|
||||||
|
dialect: config.dialect,
|
||||||
|
port: config.PORT,
|
||||||
|
pool: {
|
||||||
|
max: config.pool.max,
|
||||||
|
min: config.pool.min,
|
||||||
|
acquire: config.pool.acquire,
|
||||||
|
idle: config.pool.idle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const db = {};
|
||||||
|
|
||||||
|
db.Sequelize = Sequelize;
|
||||||
|
db.sequelize = sequelize;
|
||||||
|
|
||||||
|
db.user = require('../models/user.model')(sequelize, Sequelize);
|
||||||
|
db.role = require('../models/role.model')(sequelize, Sequelize);
|
||||||
|
db.bill = require('../models/bill.model')(sequelize, Sequelize);
|
||||||
|
|
||||||
|
db.role.belongsToMany(db.user, {
|
||||||
|
through: "user_roles"
|
||||||
|
});
|
||||||
|
|
||||||
|
db.user.belongsToMany(db.role, {
|
||||||
|
through: "user_roles"
|
||||||
|
});
|
||||||
|
|
||||||
|
db.ROLES = ["user", "admin", "moderator"];
|
||||||
|
|
||||||
|
module.exports = db;
|
||||||
19
src/models/role.model.js
Normal file
19
src/models/role.model.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
module.exports = (sequelize, DataTypes) => {
|
||||||
|
const Role = sequelize.define("roles", {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: "roles",
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return Role;
|
||||||
|
}
|
||||||
15
src/models/user.model.js
Normal file
15
src/models/user.model.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
module.exports = (sequelize, Sequelize) => {
|
||||||
|
const User = sequelize.define("users", {
|
||||||
|
username: {
|
||||||
|
type: Sequelize.STRING
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: Sequelize.STRING
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: Sequelize.STRING
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return User;
|
||||||
|
}
|
||||||
9
src/routes/auth.routes.js
Normal file
9
src/routes/auth.routes.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const controller = require("../controllers/auth.controller");
|
||||||
|
|
||||||
|
// Auth routes
|
||||||
|
router.post("/signup", controller.signup);
|
||||||
|
router.post("/signin", controller.signin);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
35
src/routes/bill.routes.js
Normal file
35
src/routes/bill.routes.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const { authJwt } = require("../middleware");
|
||||||
|
const controller = require("../controllers/bill.controller");
|
||||||
|
|
||||||
|
/* Bill model
|
||||||
|
id: PK int
|
||||||
|
name: string
|
||||||
|
amount: int
|
||||||
|
due: date
|
||||||
|
type: string
|
||||||
|
shared: bool
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Bill routes
|
||||||
|
|
||||||
|
// Get all bills
|
||||||
|
router.get("/", [authJwt.verifyToken, authJwt.isAdmin], controller.getBills);
|
||||||
|
|
||||||
|
// Get bills by month
|
||||||
|
router.get("/month/:month", [authJwt.verifyToken, authJwt.isAdmin], controller.getBillsByMonth);
|
||||||
|
|
||||||
|
// Get bill by id
|
||||||
|
router.get("/:id", [authJwt.verifyToken, authJwt.isAdmin], controller.getBillById);
|
||||||
|
|
||||||
|
// Create bill
|
||||||
|
router.post("/", [authJwt.verifyToken, authJwt.isAdmin], controller.createBill);
|
||||||
|
|
||||||
|
// Delete bill by id
|
||||||
|
router.delete("/:id", [authJwt.verifyToken, authJwt.isAdmin], controller.deleteBill);
|
||||||
|
|
||||||
|
// Update bill by id
|
||||||
|
router.put("/:id", [authJwt.verifyToken, authJwt.isAdmin], controller.updateBill);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
20
src/routes/user.routes.js
Normal file
20
src/routes/user.routes.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const { authJwt } = require("../middleware");
|
||||||
|
const controller = require("../controllers/user.controller");
|
||||||
|
|
||||||
|
// User routes
|
||||||
|
|
||||||
|
// All access page
|
||||||
|
router.get("/all", /*[authJwt.verifyToken, authJwt.isAdmin],*/ controller.allAccess);
|
||||||
|
|
||||||
|
// User access page
|
||||||
|
router.get("/user", [authJwt.verifyToken], controller.userBoard);
|
||||||
|
|
||||||
|
// Mod access page
|
||||||
|
router.get("/mod", [authJwt.verifyToken, authJwt.isModerator], controller.moderatorBoard);
|
||||||
|
|
||||||
|
// Admin access page
|
||||||
|
router.get("/admin", [authJwt.verifyToken, authJwt.isAdmin], controller.adminBoard);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
127
src/server.js
Normal file
127
src/server.js
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
require("dotenv").config();
|
||||||
|
|
||||||
|
const express = require("express");
|
||||||
|
const cors = require("cors");
|
||||||
|
const helmet = require("helmet");
|
||||||
|
const rateLimit = require("express-rate-limit");
|
||||||
|
const morgan = require("morgan");
|
||||||
|
const { sequelize, Sequelize } = require('./models');
|
||||||
|
const db = require("./models");
|
||||||
|
const Role = db.role;
|
||||||
|
|
||||||
|
const createApp = () => {
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Security headers
|
||||||
|
app.use(helmet());
|
||||||
|
|
||||||
|
// CORS restrict in production (set ALLOWED_ORIGINS env to comma-separated list)
|
||||||
|
const corsOptions = {
|
||||||
|
origin: process.env.NODE_ENV === "production"
|
||||||
|
? (process.env.ALLOWED_ORIGINS || "").split(",").map(s => s.trim())
|
||||||
|
: true,
|
||||||
|
optionsSuccessStatus: 200
|
||||||
|
};
|
||||||
|
app.use(cors(corsOptions));
|
||||||
|
|
||||||
|
// Logging verbose in dev
|
||||||
|
app.use(morgan(process.env.NODE_ENV === "production" ? "combined" : "dev"));
|
||||||
|
|
||||||
|
// Body parsing with size limits
|
||||||
|
app.use(express.json({limit: "10kb"}));
|
||||||
|
app.use(express.urlencoded({extended: false, limit: "10kb"}));
|
||||||
|
|
||||||
|
// Apply rate limiting in production
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
app.use(rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100, // limit per IP
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get("/health", (req, res) => res.status(200).send({status: "ok"}));
|
||||||
|
|
||||||
|
// Mount routes which should have an Express Router
|
||||||
|
app.use("/auth", require("./routes/auth.routes"));
|
||||||
|
app.use("/user", require("./routes/user.routes"));
|
||||||
|
app.use("/bills", require("./routes/bill.routes"));
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use((req, res) => res.status(404).send({message: "Not Found"}));
|
||||||
|
|
||||||
|
// Error handler (MUST BE LAST)
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status || 500).send({message: err.message || "Internal Server Error"});
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize roles safely
|
||||||
|
const initRoles = async () => {
|
||||||
|
const roles = ["user", "moderator", "admin"];
|
||||||
|
for (const name of roles) {
|
||||||
|
await Role.findOrCreate({where: {name}, defaults: {name}});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = async () => {
|
||||||
|
try {
|
||||||
|
// Validate important env vars
|
||||||
|
if (!process.env.JWT_SECRET) {
|
||||||
|
console.warn("JWT_SECRET not set. Ensure secure secret in production.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync DB (not forced in production)
|
||||||
|
const doForce = process.env.FORCE_DB_SYNC === "true" && process.env.NODE_ENV !== "production";
|
||||||
|
await sequelize.authenticate();
|
||||||
|
await sequelize.sync({force: doForce});
|
||||||
|
if (doForce) console.info("Database dropped and resynced (force=true).");
|
||||||
|
|
||||||
|
await initRoles();
|
||||||
|
|
||||||
|
const app = createApp();
|
||||||
|
|
||||||
|
// Set port and listen
|
||||||
|
const PORT = parseInt(process.env.PORT, 10) || 5000;
|
||||||
|
const server = app.listen(PORT, () => {
|
||||||
|
console.log(`Server listening on port ${PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
const shutdown = async (signal) => {
|
||||||
|
console.info(`Received signal: ${signal}. Shutting down server...`);
|
||||||
|
server.close(async () => {
|
||||||
|
try {
|
||||||
|
await sequelize.close();
|
||||||
|
console.info("Database connection closed. Exiting.");
|
||||||
|
process.exit(0);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error during shutdown:", err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Force exit after timeout
|
||||||
|
setTimeout(() => {
|
||||||
|
console.error("Could not close connections in time, forcing shut down");
|
||||||
|
process.exit(1);
|
||||||
|
}, 30_000);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||||
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to start server:", err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
start();
|
||||||
|
|
||||||
|
// Export createApp for tests
|
||||||
|
module.exports = { createApp };
|
||||||
Loading…
Reference in New Issue
Block a user