π All students have paid their fees. No pending dues!
- <% } else { %> <%for(let students of studentsData){%> - -- <%= students.name %> -
-GRADE : <%= students.grade %>
-- DUE FEES : <%= students.dueFees %> -
-diff --git a/.github/workflows/monthlydue.yml b/.github/workflows/monthlydue.yml new file mode 100644 index 0000000..38efaa7 --- /dev/null +++ b/.github/workflows/monthlydue.yml @@ -0,0 +1,27 @@ +name: Monthly Due Update + +on: + schedule: + - cron: "30 00 * * *" + workflow_dispatch: # Allows manual run + +jobs: + run-monthly-job: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: "18" + + - name: Install Dependencies + run: npm install + + - name: Run Monthly Due Script + env: + MONGO_URL: ${{ secrets.MONGO_URL }} + run: node services/cronJobs.js diff --git a/controllers/fund.js b/controllers/fund.js new file mode 100644 index 0000000..6c61af7 --- /dev/null +++ b/controllers/fund.js @@ -0,0 +1,131 @@ +const Student = require("../models/students"); +const catchAsync = require("../utils/catchAsync"); +const MonthlyReport = require("../models/monthlyReport"); +let thisMonthYear = new Date().toLocaleString("en-US", { + month: "short", + year: "numeric", +}); +module.exports.fund = catchAsync(async (req, res) => { + const now = new Date(); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1); + const [dueResult, feesThisMonth, stuThisMonth] = await Promise.all([ + // πΉ Total due (all students) + Student.aggregate([ + { $match: { owner: req.user._id } }, + { + $group: { + _id: null, + totalDue: { $sum: "$dueFees" }, + }, + }, + ]), + Student.aggregate([ + { $match: { owner: req.user._id } }, + { $unwind: "$feesHistory" }, + + { + $match: { + "feesHistory.paidDate": { $gte: startOfMonth, $lt: endOfMonth }, + }, + }, + + { + $sort: { + "feesHistory.paidDate": -1, // ascending + }, + }, + + { + $project: { + name: 1, + grade: 1, + "feesHistory.amount": 1, + "feesHistory.paidDate": 1, + "feesHistory.note": 1, + }, + }, + ]), + Student.find({ + owner: req.user._id, + joiningDate: { + $gte: startOfMonth, + $lt: endOfMonth, + }, + }).select("name grade"), + ]); + const todayDate = new Date().toISOString().split("T")[0]; + const totalDue = dueResult[0]?.totalDue || 0; + const month = new Date().getMonth() + 1; + const year = new Date().getFullYear(); + let thisMonthData = await MonthlyReport.findOne({ + owner: req.user._id, + month, + year, + }); + if (!thisMonthData) { + thisMonthData = await MonthlyReport.create({ + owner: req.user._id, + month, + year, + totalEarning: 0, + expenses: [], + totalExpenses: 0, + newStudents: 0, + studentsLeft: 0, + createdAt: new Date(), + }); + } + const previousReport = await MonthlyReport.find({ + owner: req.user._id, + $or: [ + { year: { $lt: year } }, // any past year + { year: year, month: { $lt: month } }, // same year but earlier months + ], + }).sort({ year: -1, month: -1 }); + res.render("listings/fund", { + totalDue, + todayDate, + feesThisMonth, + previousReport, + thisMonthYear, + thisMonthData, + stuThisMonth, + }); +}); +module.exports.addExpense = catchAsync(async (req, res) => { + const { note, amount, paidDate } = req.body; + if (!amount || amount <= 0) { + req.flash("error", "Amount must be greater than 0"); + return res.redirect(`/fund`); + } + const month = new Date().getMonth() + 1; + const year = new Date().getFullYear(); + const report = await MonthlyReport.findOneAndUpdate( + { owner: req.user._id, month, year }, + { + $setOnInsert: { + owner: req.user._id, + month, + year, + totalEarning: 0, + expenses: [], + totalExpenses: 0, + newStudents: 0, + studentsLeft: 0, + createdAt: new Date(), + }, + }, + { new: true, upsert: true }, + ); + console.log("report of the month"); + console.log(month, year, report); + report.expenses.push({ + note, + amount: Number(amount), + paidDate: paidDate ? new Date(paidDate) : new Date(), + }); + await report.save(); + req.flash("success", "Expense added successfully"); + res.redirect("/fund"); +}); diff --git a/controllers/students.js b/controllers/students.js index fcbea71..2ddcada 100644 --- a/controllers/students.js +++ b/controllers/students.js @@ -1,77 +1,82 @@ -const mongoose = require("mongoose"); const Student = require("../models/students"); const catchAsync = require("../utils/catchAsync"); +const MonthlyReport = require("../models/monthlyReport"); +function formatDate(date, type = "short") { + if (!date) return ""; + if (type === "input") return date.toISOString().split("T")[0]; + if (type === "today") return new Date().toISOString().substr(0, 10); + return date.toLocaleDateString("en-GB", { + weekday: "short", + day: "2-digit", + month: "short", + year: "numeric", + }); +} module.exports.students = catchAsync(async (req, res) => { - const students = await Student.find({}); + const students = await Student.find({ owner: req.user._id }); res.render("listings/students", { studentsData: students }); }); module.exports.showStudent = catchAsync(async (req, res, next) => { const { id } = req.params; - // validate id - if (!mongoose.Types.ObjectId.isValid(id)) { - req.flash("error", "Invalid student id"); - return res.redirect("/students"); - } - const student = await Student.findById(id); - const formattedDate = student.joiningDate.toLocaleDateString("en-GB", { - weekday: "short", // Fri - day: "2-digit", // 15 - month: "short", // Aug - year: "numeric", // 2025 - }); - const todayDate = new Date().toISOString().substr(0, 10); - if (!student) { - req.flash("error", "Student not found"); - return res.redirect("/students"); - } - res.render("listings/show", { student, formattedDate, todayDate }); + const student = await Student.findOne({ _id: id, owner: req.user._id }); + if (!student) throw new ExpressError(404, "Student not found"); + const formattedDate = formatDate(student.joiningDate); + res.render("listings/show", { student, formattedDate }); }); module.exports.editStudent = catchAsync(async (req, res, next) => { const { id } = req.params; - // validate id - if (!mongoose.Types.ObjectId.isValid(id)) { - req.flash("error", "Invalid student id"); - return res.redirect("/students"); - } - const student = await Student.findById(id); - if (!student) { - req.flash("error", "Student not found"); - return res.redirect("/students"); - } - - // Convert to YYYY-MM-DD for input type="date" - const formattedDate = student.joiningDate - ? student.joiningDate.toISOString().split("T")[0] - : ""; + const student = await Student.findOne({ _id: id, owner: req.user._id }); + if (!student) throw new ExpressError(404, "Student not found"); + const formattedDate = formatDate(student.joiningDate, "input"); res.render("listings/edit", { student, formattedDate }); // create this view }); module.exports.saveEditStudent = catchAsync(async (req, res, next) => { const { id } = req.params; - const { name, grade, parent, contact, phone, note, joiningDate, fees } = - req.body; - let updatedStudent = await Student.findByIdAndUpdate(id, { - name: name, - grade: grade, - parent: parent, - contact: contact, - phone: phone, - note: note, - joiningDate: joiningDate, - fees: fees, - }); - const student = await Student.findById(id); - const formattedDate = student.joiningDate.toLocaleDateString("en-GB", { - weekday: "short", // Fri - day: "2-digit", // 15 - month: "short", // Aug - year: "numeric", // 2025 + const { + name, + grade, + parent, + contact, + phone, + note, + joiningDate, + fees, + dueFees, + } = req.body; + // const student = await Student.findOne({ _id: id, owner: req.user._id }); + // if (!student) throw new ExpressError(404, "Student not found"); + let student = await Student.findOneAndUpdate( + { _id: id, owner: req.user._id }, + { + name, + grade, + parent, + contact, + phone, + note, + joiningDate, + dueFees, + fees, + }, + { new: true, runValidators: true }, + ); + if (!student) { + req.flash("error", "Student not found"); + return res.redirect("/students"); + } + const formattedDate = formatDate(student.joiningDate); + // req.flash("success", "Details updated."); + // res.redirect(`/students/${updatedStudent._id}`); + res.render("listings/show", { + student, + formattedDate, + // todayDate, + success: "Details updated.", }); - res.render("listings/show", { student, formattedDate }); }); module.exports.newStudentForm = (req, res) => { - const todayDate = new Date().toISOString().substr(0, 10); - res.render("listings/newStudent", { todayDate }); + res.render("listings/newStudent"); }; module.exports.addNewStudent = catchAsync(async (req, res) => { const { @@ -84,8 +89,10 @@ module.exports.addNewStudent = catchAsync(async (req, res) => { note, joiningDate, fees, + dueFees, } = req.body; const newStudent = new Student({ + owner: req.user._id, name, grade, status, @@ -95,38 +102,118 @@ module.exports.addNewStudent = catchAsync(async (req, res) => { note, joiningDate, fees, + dueFees, + }); + const month = new Date().getMonth() + 1; + const year = new Date().getFullYear(); + let thisMonthData = await MonthlyReport.findOne({ + owner: req.user._id, + month, + year, }); + if (!thisMonthData) { + thisMonthData = await MonthlyReport.create({ + owner: req.user._id, + month, + year, + totalEarning: 0, + expenses: [], + totalExpenses: 0, + newStudents: 0, + studentsLeft: 0, + createdAt: new Date(), + }); + } + thisMonthData.newStudents += 1; + await thisMonthData.save(); await newStudent.save(); - console.log("Student saved"); + req.flash("success", `New student added`); res.redirect("/students"); }); module.exports.deleteStudent = catchAsync(async (req, res) => { let { id } = req.params; - let removeStudent = await Student.findByIdAndDelete(id); - if (!removeStudent) { - throw new ExpressError(404, "Student not found"); + const month = new Date().getMonth() + 1; + const year = new Date().getFullYear(); + let thisMonthData = await MonthlyReport.findOne({ + owner: req.user._id, + month, + year, + }); + if (!thisMonthData) { + thisMonthData = await MonthlyReport.create({ + owner: req.user._id, + month, + year, + totalEarning: 0, + expenses: [], + totalExpenses: 0, + newStudents: 0, + studentsLeft: 0, + createdAt: new Date(), + }); + } + thisMonthData.studentsLeft += 1; + await thisMonthData.save(); + let removedStudent = await Student.findOneAndDelete({ + _id: id, + owner: req.user._id, + }); + if (!removedStudent) { + req.flash("error", "Student not found or already deleted."); + return res.redirect("/students"); } + req.flash("success", `${removedStudent.name} has been removed.`); res.redirect("/students"); }); module.exports.addFees = catchAsync(async (req, res) => { let { id } = req.params; - const { note, amount, paidOn } = req.body; - const student = await Student.findById(id); + let { note, amount, paidOn } = req.body; + if (!amount || Number(amount) <= 0) { + req.flash("error", "Amount must be greater than 0"); + return res.redirect(`/students/${id}`); + } + amount = Number(amount); + let paidDate = paidOn ? new Date(paidOn) : new Date(); + const student = await Student.findOne({ _id: id, owner: req.user._id }); if (!student) { return res.status(404).send("Student not found"); } - // Add fees entry to the student's feesHistory student.feesHistory.push({ note, amount, - paidDate: paidOn ? new Date(paidOn) : new Date(), + paidDate, + }); + const month = new Date().getMonth() + 1; + const year = new Date().getFullYear(); + let thisMonthData = await MonthlyReport.findOne({ + owner: req.user._id, + month, + year, }); + if (!thisMonthData) { + thisMonthData = await MonthlyReport.create({ + owner: req.user._id, + month, + year, + totalEarning: 0, + expenses: [], + totalExpenses: 0, + newStudents: 0, + studentsLeft: 0, + createdAt: new Date(), + }); + } + thisMonthData.totalEarning += amount; student.dueFees -= amount; await student.save(); - console.log("Fees added successfully"); + await thisMonthData.save(); + req.flash("success", `Fees added for ${student.name}. `); res.redirect(`/students/${id}`); }); module.exports.dashboard = catchAsync(async (req, res) => { - const students = await Student.find({ dueFees: { $gt: 0 } }); - res.render("listings/dashboard", { studentsData: students }); + const students = await Student.find({ + owner: req.user._id, + dueFees: { $gt: 0 }, + }); + res.render("listings/students", { studentsData: students }); }); diff --git a/controllers/user.js b/controllers/user.js new file mode 100644 index 0000000..15e30b8 --- /dev/null +++ b/controllers/user.js @@ -0,0 +1,92 @@ +const userModel = require("../models/user.js"); +const bcrypt = require("bcrypt"); +const jwt = require("jsonwebtoken"); + +module.exports.renderSignupForm = (req, res) => { + res.render("./users/signup.ejs"); +}; + +module.exports.signup = async (req, res) => { + try { + const { username, email, password } = req.body; + // Check if user already exists + const existingUser = await userModel.findOne({ email }); + if (existingUser) { + req.flash("error", "User already exists"); + return res.redirect("/signup"); + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 10); + + // Create user + const createdUser = await userModel.create({ + username, + email, + password: hashedPassword, + createdAt: new Date(), + }); + + // Generate JWT + const token = jwt.sign({ id: createdUser._id }, process.env.JWT_SECRET, { + expiresIn: "7d", + }); + + // Save token in cookie + res.cookie("token", token, { + httpOnly: true, + secure: false, // true in production + sameSite: "lax", + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + + req.flash("success", "Welcome to Student Management System"); + res.redirect("/students"); + } catch (e) { + console.error(e); + req.flash("error", "Signup failed"); + res.redirect("/signup"); + } +}; + +module.exports.renderloginForm = (req, res) => { + res.render("./users/login.ejs"); +}; + +module.exports.login = async (req, res) => { + try { + const { email, password } = req.body; + let user = await userModel.findOne({ email }); + if (!user) { + req.flash("error", "Invalid email or password"); + return res.redirect("/login"); + } + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + req.flash("error", "Invalid email or password"); + return res.redirect("/login"); + } + const token = jwt.sign( + { id: user._id }, + process.env.JWT_SECRET || "your-secret-key", + { expiresIn: "7d" }, + ); + res.cookie("token", token, { + httpOnly: true, + secure: false, // true in production (https) + sameSite: "lax", + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + req.flash("success", "Welcome back to Student Management System"); + res.redirect("/students"); + } catch (error) { + console.error(error); + req.flash("error", "Login failed"); + res.redirect("/login"); + } +}; +module.exports.logout = (req, res) => { + res.clearCookie("token"); + req.flash("success", "Logged out successfully"); + res.redirect("/home"); +}; diff --git a/index.js b/index.js index b93071e..7b71cdd 100644 --- a/index.js +++ b/index.js @@ -2,59 +2,86 @@ if (process.env.NODE_ENV != "production") { require("dotenv").config(); } const express = require("express"); -const mongoose = require("mongoose"); -const app = express(); const path = require("path"); -const students = require("./routes/students.js"); -const Student = require("./models/students"); -const mehtodOverride = require("method-override"); -app.use(mehtodOverride("_method")); +const mongoose = require("mongoose"); -app.set("views", path.join(__dirname, "views")); -app.use(express.static(path.join(__dirname, "public"))); +const session = require("express-session"); +const MongoStore = require("connect-mongo"); -app.set("view engine", "ejs"); -const ejsMate = require("ejs-mate"); -app.engine("ejs", ejsMate); +// npm uninstall express-session connect-mongo +const cookieParser = require("cookie-parser"); -app.use(express.urlencoded({ extended: true })); -const MONGO_URL = process.env.ATLASDB_URL; -// const MONGO_URL = "mongodb://127.0.0.1:27017/DynamicVision"; -main() - .then(() => { - console.log("Connected to DB"); - }) - .catch((err) => { - console.log("DB Connection Error:", err); - }); -async function main() { - await mongoose.connect(MONGO_URL); -} -async function addMonthlyDueFees() { - try { - const students = await Student.find(); +const flash = require("connect-flash"); +const ejsMate = require("ejs-mate"); - for (let student of students) { - if (!student.joiningDate) continue; +const students = require("./routes/students"); +const setTodayDate = require("./middleware/setTodayDate"); +const { dashboard } = require("./controllers/students.js"); +const { fund } = require("./controllers/fund.js"); +const user = require("./routes/user.js"); +const { isLoggedIn } = require("./middleware/isLoggedIn"); +const mehtodOverride = require("method-override"); +const app = express(); +app.use(mehtodOverride("_method")); - const today = new Date(); - const joinDay = student.joiningDate.getDate(); // e.g., 15 - // if today is student's due day - if (today.getDate() === joinDay) { - student.dueFees += student.fees; // add monthly fee - await student.save(); - console.log(`Added due fee for ${student.name}`); - } - } +/* ---------- Config ---------- */ +const PORT = process.env.PORT || 8000; +const MONGO_URL = process.env.MONGO_URL; +/* ---------- Mongoose ---------- */ +async function connectDB() { + try { + await mongoose.connect(MONGO_URL); + console.log("Connected to MongoDB"); } catch (err) { - console.error("Error in auto due fees:", err); + console.error("MongoDB connection error:", err); + process.exit(1); } } -const cron = require("node-cron"); -const { dashboard } = require("./controllers/students.js"); -// Run every day at midnight -cron.schedule("0 0 * * *", () => { - addMonthlyDueFees(); +connectDB(); + +/* ---------- View engine & static ---------- */ +app.engine("ejs", ejsMate); +app.set("view engine", "ejs"); +app.set("views", path.join(__dirname, "views")); +app.use(express.static(path.join(__dirname, "public"))); +app.use(express.urlencoded({ extended: true })); +app.use(cookieParser()); +const attachUser = require("./middleware/attachUser"); +app.use(attachUser); + +const store = MongoStore.create({ + mongoUrl: MONGO_URL, + ttl: 7 * 24 * 60 * 60, + crypto: { + secret: process.env.SECRET, + }, + touchAfter: 24 * 3600, +}); +store.on("error", (err) => { + console.log("ERROR in Mongo Session Store:", err); +}); +const sessionOptions = { + store, + name: "session", + secret: process.env.SECRET, + resave: false, + saveUninitialized: false, + cookie: { + httpOnly: true, + maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days + expires: Date.now() + 1000 * 60 * 60 * 24 * 7, + }, +}; + +app.use(session(sessionOptions)); +app.use(flash()); + +app.use(setTodayDate); +app.use((req, res, next) => { + res.locals.success = req.flash("success"); + res.locals.error = req.flash("error"); + res.locals.currUser = req.user; //auth/passport + next(); }); app.get("/home", (req, res) => { @@ -63,12 +90,14 @@ app.get("/home", (req, res) => { app.get("/blog", (req, res) => { res.render("listings/blog.ejs"); }); -app.use("/dashboard", dashboard); -app.use("/students", students); +app.use("/dashboard", isLoggedIn, dashboard); +app.use("/fund", isLoggedIn, fund); +app.use("/students", isLoggedIn, students); +app.use("/", user); app.use((err, req, res, next) => { console.error(err.stack); - res.status(500).send("Something went wrong! π¨"); + res.status(500).send("Something went wrong!"); }); -app.listen(8000, () => { - console.log(`App is listing to port : 8000`); +app.listen(PORT, () => { + console.log(`App is listing to port : ${PORT}`); }); diff --git a/middleware/attachUser.js b/middleware/attachUser.js new file mode 100644 index 0000000..f232395 --- /dev/null +++ b/middleware/attachUser.js @@ -0,0 +1,26 @@ +const jwt = require("jsonwebtoken"); +const User = require("../models/user"); + +module.exports = async (req, res, next) => { + const token = req.cookies?.token; + + if (!token) { + req.user = null; + res.locals.currUser = null; + return next(); + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + const user = await User.findById(decoded.id).select("-password"); + + req.user = user; + res.locals.currUser = user; + next(); + } catch (err) { + res.clearCookie("token"); + req.user = null; + res.locals.currUser = null; + next(); + } +}; diff --git a/middleware/isLoggedIn.js b/middleware/isLoggedIn.js new file mode 100644 index 0000000..cbb8beb --- /dev/null +++ b/middleware/isLoggedIn.js @@ -0,0 +1,35 @@ +const jwt = require("jsonwebtoken"); +const userModel = require("../models/user"); + +module.exports.isLoggedIn = async (req, res, next) => { + try { + const token = req.cookies.token; + + // 1οΈβ£ Check token exists + if (!token) { + req.flash("error", "Please login first"); + return res.redirect("/login"); + } + + // 2οΈβ£ Verify token + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + // 3οΈβ£ Find user + const user = await userModel.findById(decoded.id); + if (!user) { + res.clearCookie("token"); + req.flash("error", "Session expired. Please login again"); + return res.redirect("/login"); + } + + // 4οΈβ£ Attach user to request + req.user = user; + // req.user = decoded; + next(); + } catch (err) { + console.error("Auth error:", err.message); + res.clearCookie("token"); + req.flash("error", "Please login again"); + res.redirect("/login"); + } +}; diff --git a/middleware/setTodayDate.js b/middleware/setTodayDate.js new file mode 100644 index 0000000..638252f --- /dev/null +++ b/middleware/setTodayDate.js @@ -0,0 +1,7 @@ +function getTodayIso() { + return new Date().toISOString().split("T")[0]; // YYYY-MM-DD +} +module.exports = (req, res, next) => { + res.locals.todayDate = getTodayIso(); + next(); +}; diff --git a/middleware/validateObjectId.js b/middleware/validateObjectId.js new file mode 100644 index 0000000..baaeb58 --- /dev/null +++ b/middleware/validateObjectId.js @@ -0,0 +1,9 @@ +const mongoose = require("mongoose"); +module.exports = (req, res, next) => { + const { id } = req.params; + if (id && !mongoose.Types.ObjectId.isValid(id)) { + req.flash("error", "Invalid ID"); + return res.redirect("/students"); + } + next(); +}; diff --git a/models/monthlyReport.js b/models/monthlyReport.js new file mode 100644 index 0000000..19deeca --- /dev/null +++ b/models/monthlyReport.js @@ -0,0 +1,31 @@ +const mongoose = require("mongoose"); + +const monthlyReportSchema = new mongoose.Schema({ + owner: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + month: { type: Number, required: true }, // 1β12 + year: { type: Number, required: true }, + + totalEarning: { type: Number, default: 0 }, + + expenses: [ + { + note: { type: String, required: true }, + amount: { type: Number, required: true }, + paidDate: { type: Date, default: Date.now }, + }, + ], + totalExpenses: { type: Number, default: 0 }, + + newStudents: { type: Number, default: 0 }, + studentsLeft: { type: Number, default: 0 }, + + createdAt: { type: Date, default: Date.now }, +}); + +// prevent duplicate month +monthlyReportSchema.index({ owner: 1, month: 1, year: 1 }, { unique: true }); +module.exports = mongoose.model("MonthlyReport", monthlyReportSchema); diff --git a/models/students.js b/models/students.js index 674cc53..985641e 100644 --- a/models/students.js +++ b/models/students.js @@ -1,6 +1,11 @@ const mongoose = require("mongoose"); const studentSchema = new mongoose.Schema({ + owner: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, name: { type: String, require: true }, grade: { type: String, require: true }, parent: { type: String }, @@ -19,6 +24,7 @@ const studentSchema = new mongoose.Schema({ paidDate: { type: Date, default: Date.now }, }, ], + lastDueAdded: { type: Date, default: null }, //sibling info, fees payment history(amount paid & date) }); diff --git a/models/user.js b/models/user.js new file mode 100644 index 0000000..7a2b384 --- /dev/null +++ b/models/user.js @@ -0,0 +1,20 @@ +const mongoose = require("mongoose"); + +const userSchema = new mongoose.Schema({ + email: { + type: String, + require: true, + unique: true, + }, + username: { + type: String, + require: true, + }, + password: { + type: String, + require: true, + }, + createdAt: { type: Date, default: Date.now }, +}); + +module.exports = mongoose.model("User", userSchema); diff --git a/owenr.js b/owenr.js new file mode 100644 index 0000000..eda6bc4 --- /dev/null +++ b/owenr.js @@ -0,0 +1,28 @@ +const mongoose = require("mongoose"); +const Student = require("./models/monthlyReport"); +// const Student = require("./models/students"); +const User = require("./models/user"); + +// mongoose.connect("mongodb://127.0.0.1:27017/DynamicVision"); + +(async () => { + try { + const owner = await User.findOne({ email: "demo@gmail.com" }); + if (!owner) { + console.log("β Owner not found"); + process.exit(); + } + + const result = await Student.updateMany( + { owner: { $exists: false } }, // only students without owner + { $set: { owner: owner._id } } + ); + + console.log("β Migration complete"); + console.log("Modified:", result.modifiedCount); + } catch (err) { + console.error(err); + } finally { + mongoose.connection.close(); + } +})(); diff --git a/package-lock.json b/package-lock.json index 2ddda8c..9c1223f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,23 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "dotenv": "^17.2.2", + "bcrypt": "^6.0.0", + "connect-flash": "^0.1.1", + "connect-mongo": "^5.1.0", + "cookie-parser": "^1.4.7", + "dotenv": "^17.2.3", "ejs": "^3.1.10", "ejs-mate": "^4.0.0", "express": "^5.1.0", + "express-mongo-sanitize": "^2.2.0", + "express-session": "^1.18.2", + "helmet": "^8.1.0", + "jsonwebtoken": "^9.0.3", "method-override": "^3.0.0", "mongodb": "^6.18.0", - "mongoose": "^8.17.0", - "node-cron": "^4.2.1" + "mongoose": "^8.19.1", + "morgan": "^1.10.1", + "passport-local": "^1.0.0" } }, "node_modules/@mongodb-js/saslprep": { @@ -66,6 +75,17 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -76,23 +96,63 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==" + }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { @@ -112,10 +172,17 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -183,6 +250,30 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/connect-flash": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz", + "integrity": "sha512-2rcfELQt/ZMP+SM/pG8PyhJRaLKp+6Hk2IUBNkEit09X+vwn3QsAL3ZbYtxUn7NVPzbMTSLRDhqe0B/eh30RYA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/connect-mongo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz", + "integrity": "sha512-xT0vxQLqyqoUTxPLzlP9a/u+vir0zNkhiy9uAdHjSCcUUf7TS5b55Icw8lVyYFxfemP3Mf9gdwUOgeF3cxCAhw==", + "dependencies": { + "debug": "^4.3.1", + "kruptein": "^3.0.0" + }, + "engines": { + "node": ">=12.9.0" + }, + "peerDependencies": { + "express-session": "^1.17.1", + "mongodb": ">= 5.1.0 < 7" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -210,6 +301,25 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -219,9 +329,10 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -243,9 +354,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", - "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "engines": { "node": ">=12" }, @@ -266,6 +377,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -385,6 +505,50 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-mongo-sanitize": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/express-mongo-sanitize/-/express-mongo-sanitize-2.2.0.tgz", + "integrity": "sha512-PZBs5nwhD6ek9ZuP+W2xmpvcrHwXZxD5GdieX2dsjPbAbH4azOkrHbycBud2QRU+YQF1CT+pki/lZGedHgo/dQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -528,38 +692,48 @@ "node": ">= 0.4" } }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", "engines": { - "node": ">= 0.8" + "node": ">=18.0.0" } }, - "node_modules/http-errors/node_modules/statuses": { + "node_modules/http-errors": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "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.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "license": "MIT", "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": { @@ -597,6 +771,49 @@ "node": ">=10" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -605,6 +822,59 @@ "node": ">=12.0.0" } }, + "node_modules/kruptein": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.8.tgz", + "integrity": "sha512-0CyalFA0Cjp3jnziMp0u1uLZW2/ouhQ0mEMfYlroBXNe86na1RwAuwBcdRAegeWZNMfQy/G5fN47g/Axjtqrfw==", + "dependencies": { + "asn1.js": "^5.4.1" + }, + "engines": { + "node": ">8" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -691,6 +961,11 @@ "node": ">= 0.6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -703,13 +978,13 @@ } }, "node_modules/mongodb": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", - "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", "dependencies": { - "@mongodb-js/saslprep": "^1.1.9", + "@mongodb-js/saslprep": "^1.3.0", "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.0" + "mongodb-connection-string-url": "^3.0.2" }, "engines": { "node": ">=16.20.1" @@ -720,7 +995,7 @@ "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", + "snappy": "^7.3.2", "socks": "^2.7.1" }, "peerDependenciesMeta": { @@ -757,13 +1032,13 @@ } }, "node_modules/mongoose": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.17.0.tgz", - "integrity": "sha512-mxW6TBPHViORfNYOFXCVOnT4d5aRr+CgDxTs1ViYXfuHzNpkelgJQrQa+Lz6hofoEQISnKlXv1L3ZnHyJRkhfA==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.1.tgz", + "integrity": "sha512-oB7hGQJn4f8aebqE7mhE54EReb5cxVgpCxQCQj0K/cK3q4J3Tg08nFP6sM52nJ4Hlm8jsDnhVYpqIITZUAhckQ==", "dependencies": { "bson": "^6.10.4", "kareem": "2.6.3", - "mongodb": "~6.18.0", + "mongodb": "~6.20.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -777,6 +1052,45 @@ "url": "https://opencollective.com/mongoose" } }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", @@ -809,12 +1123,24 @@ "node": ">= 0.6" } }, - "node_modules/node-cron": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", - "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" } }, "node_modules/object-inspect": { @@ -839,6 +1165,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -855,6 +1189,25 @@ "node": ">= 0.8" } }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -884,9 +1237,10 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" }, @@ -897,6 +1251,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -906,17 +1268,18 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/router": { @@ -958,6 +1321,18 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -1130,10 +1505,22 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "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", "engines": { "node": ">= 0.8" } diff --git a/package.json b/package.json index a012988..04fa833 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,22 @@ "license": "ISC", "description": "", "dependencies": { - "dotenv": "^17.2.2", + "bcrypt": "^6.0.0", + "connect-flash": "^0.1.1", + "connect-mongo": "^5.1.0", + "cookie-parser": "^1.4.7", + "dotenv": "^17.2.3", "ejs": "^3.1.10", "ejs-mate": "^4.0.0", "express": "^5.1.0", + "express-mongo-sanitize": "^2.2.0", + "express-session": "^1.18.2", + "helmet": "^8.1.0", + "jsonwebtoken": "^9.0.3", "method-override": "^3.0.0", "mongodb": "^6.18.0", - "mongoose": "^8.17.0", - "node-cron": "^4.2.1" + "mongoose": "^8.19.1", + "morgan": "^1.10.1", + "passport-local": "^1.0.0" } } diff --git a/public/addStudent.png b/public/addStudent.png new file mode 100644 index 0000000..969959e Binary files /dev/null and b/public/addStudent.png differ diff --git a/public/css/blog.css b/public/css/blog.css index f947ad7..596f92a 100644 --- a/public/css/blog.css +++ b/public/css/blog.css @@ -1,8 +1,3 @@ -@font-face { - font-family: font; - src: url(../MethanerseFreeTrial-0WpE4.otf); -} - * { padding: 0; margin: 0; diff --git a/public/css/flash.css b/public/css/flash.css new file mode 100644 index 0000000..e4847be --- /dev/null +++ b/public/css/flash.css @@ -0,0 +1,95 @@ +*{ + font-family: sans-serif; +} +.notifications { + position: fixed; + top: 16px; + right: 12px; + z-index: 10; +} +.toast { + position: relative; + padding: 15px; + color: #fff; + color: red; + margin-bottom: 10px; + /* width: 300px; */ + display: grid; + grid-template-columns: 40px 1fr 25px; + border-radius: 5px; + --color: #0abf30; + background-image: linear-gradient(to right, #0abf3055, #31312f 30%); + background-color: linear-gradient(to right, #0abf3055, #31312f 30%); + background-color: #31312f; + animation: show 0.3s ease 1 forwards; +} +.toast i { + color: var(--color); + display: flex; + justify-content: center; + align-items: center; + font-size: x-large; +} +.toast .title { + font-size: x-large; + font-weight: 400; + color: #fff; + padding-right: 15px; +} +.toast span, +.toast i:nth-child(3) { + color: #fff; + opacity: 0.6; +} +@keyframes show { + 0% { + transform: translateX(100%); + } + 40% { + transform: translateX(-5%); + } + 80% { + transform: translateX(0%); + } + 100% { + transform: translateX(-10%); + } +} +.toast::before { + position: absolute; + bottom: 0; + left: 0; + background-color: var(--color); + width: 100%; + height: 3px; + content: ""; + box-shadow: 0 0 10px var(--color); + animation: timeOut 3s linear 1 forwards; +} +@keyframes timeOut { + to { + width: 0; + } +} +.toast.error { + --color: #f24d4c; + background-image: linear-gradient(to right, #f24d4c55, #22242f 30%); +} +.toast.warning { + --color: #e9bd0c; + background-image: linear-gradient(to right, #e9bd0c55, #22242f 30%); +} +.toast.info { + --color: #3498db; + background-image: linear-gradient(to right, #3498db55, #22242f 30%); +} +@media (max-width: 600px) { + .toast { + top: 5.7svh; + right: -6vw; + padding: 10px; + } + .toast .title{ + font-size: 4vw; + } +} diff --git a/public/css/fund.css b/public/css/fund.css new file mode 100644 index 0000000..7b44e6b --- /dev/null +++ b/public/css/fund.css @@ -0,0 +1,303 @@ +* { + padding: 0; + margin: 0; + box-sizing: border-box; +} +html, +body { + background-color: #f3efea; + scroll-behavior: smooth; + height: 100%; + width: 100%; +} +.main { + position: relative; + width: 100%; + background-color: #f3efea; +} +.container { + width: calc(100% - 6vw); + margin: 2vw 3vw; + font-family: sans-serif; +} +.nav-tabs { + color: #31312f; + font-size: 1.2rem; +} +.thisMonth { + margin: 4vh auto; + display: flex; + justify-content: space-around; + flex-wrap: wrap; +} +.monthly-reports-container { + margin: 2vw auto; + display: flex; + flex-wrap: wrap; + justify-content: space-around; +} +.report-card { + width: 27vw; + background: #31312f; + border-radius: 30px; + padding: 2rem; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); +} +.monthly-reports-container .report-card { + margin-bottom: 2vw; +} + +.report-stats { + width: 100%; +} + +.stat { + padding: 0.6rem; + color: white; + border-bottom: 1px dashed #ddd; + display: flex; + justify-content: space-around; +} +.stat .total { + border-radius: 30px; + justify-content: space-between; + padding: 0.6rem; + padding: 0.6rem 1.2rem; +} +.total span { + font-size: 2.5rem; + font-weight: 600; +} +.new { + display: flex; + justify-content: space-between; +} + +.stat div { + margin: 0; + font-size: 14px; +} + +.new-stu { + max-height: 25vh; + overflow-y: scroll; + scroll-behavior: smooth; +} +::-webkit-scrollbar { + width: 8px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: #6e6e6e; + border-radius: 10px; +} +.expense-section h3 { + margin: 10px 0; + color: white; +} + +.expense-section ul { + list-style: none; + padding: 0; +} + +.expense-section li { + display: flex; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px dashed #ddd; + color: white; +} +.addFess { + width: 27vw; +} +.history { + background-color: #31312f; + color: rgba(255, 255, 255, 0.7); + padding: 2rem; + border-radius: 30px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); +} +.feeHistory { + width: 23vw; + height: 55vh; + overflow-y: scroll; +} +.feeHistory h2 { + margin-bottom: 3vh; +} +.fee-entry { + margin: 2vh 0; + padding-bottom: 5px; + border-bottom: 1px dashed #ddd; +} +.fee-entry a { + color: white; + text-decoration: none; +} +.fee-entry p { + font-size: 1.4rem; +} + +.title { + font-size: 2.5rem; + font-weight: 600; + letter-spacing: -1px; + color: #31312f; + color: #00bfff; +} +.form { + display: flex; + flex-direction: column; + gap: 10px; + max-width: 87vh; + width: 27vw; + padding: 2rem; + border-radius: 30px; + position: relative; + background-color: #31312f; + color: rgba(255, 255, 255, 0.7); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); +} +.form label { + position: relative; + margin-top: 1.5vh; +} + +.form label .input { + background-color: #333; + color: rgba(255, 255, 255, 0.7); + width: 100%; + padding: 20px 05px 05px 10px; + outline: 0; + border: 1px solid rgba(105, 105, 105, 0.397); + border-radius: 10px; +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} +.form label .input + span { + color: rgba(255, 255, 255, 0.5); + position: absolute; + left: 10px; + top: 0px; + font-size: 0.9em; + font-size: 2rem; + cursor: text; + transition: 0.3s ease; +} + +.form label .input:placeholder-shown + span { + top: 12.5px; + font-size: 1.2em; +} + +.form label .input:focus + span, +.form label .input:valid + span { + color: #00bfff; + top: -5px; + font-size: 1.2em; + font-weight: 600; +} + +.input { + font-size: medium; + font-size: 3vh; +} + +.submit { + border: none; + outline: none; + padding: 10px; + border-radius: 10px; + color: #fff; + font-size: 3vh; + font-weight: bold; + transform: 0.3s ease; + background-color: #00bfff; +} + +.submit:hover { + background-color: #00bfff96; +} + +.student { + margin-right: 3vw; +} +@media (max-width: 600px) { + .nav-tabs h1 { + font-size: 2rem; + } + .thisMonth { + flex-direction: column; + margin: 3vw; + } + .title { + font-size: 6.5vw; + } + .report-card { + width: 100%; + padding: 3.5vw; + } + .addFess { + width: 100%; + } + .total span { + font-size: 7vw; + } + .feeHistory { + width: 100%; + max-height: 40vh; + padding: 0; + } + .history { + padding: 3.5vw; + } + .feeHistory h2 { + margin-bottom: 0vw; + } + .fee-entry { + margin: 1.5vh 0; + } + .fee-entry p { + font-size: 1rem; + } + .form { + padding: 3.5vw; + width: 100%; + } + .message { + margin-left: 1.5vw; + font-size: 5.5vw; + } + .input { + font-size: 2.5vh; + } + .submit { + font-size: 5vw; + margin-top: 15px; + } + .student { + margin-right: 0vw; + } + .addFess { + margin: 3vw 0; + } + .flex { + margin: 3vw; + } + .monthly-reports-container { + margin: 2vh 3vw; + max-height: 80vh; + overflow-y: scroll; + margin-bottom: 4vh; + } + .monthly-reports-container .report-card { + margin-bottom: 2rem; + } +} diff --git a/public/css/nav.css b/public/css/nav.css index a3db6f9..df08e69 100644 --- a/public/css/nav.css +++ b/public/css/nav.css @@ -1,3 +1,7 @@ +@font-face { + font-family: font; + src: url(../MethanerseFreeTrial-0WpE4.otf); +} nav { display: flex; align-items: center; @@ -84,13 +88,66 @@ nav a:hover::before { .info a:hover i { color: #f3efea; } +.profile, +.profile img { + border-radius: 50%; + height: 42px; + width: 42px; + cursor: pointer; + transition: all ease 0.4s; +} +.profile:hover { + box-shadow: 0px 0px 10px #31312f8f; + transform: translateY(-3px); +} +.menu { + width: 9rem; + background-color: #f3efea; + background-color: rgb(218, 218, 218); + color: #31312f; + padding: 0.4rem; + border-radius: 0.4rem; + cursor: pointer; + position: fixed; + right: 5vh; + top: 15vh; + z-index: 15; +} +.menu ul li { + padding: 0.8rem; + text-align: center; + background-color: rgb(218, 218, 218); + display: flex; + justify-content: space-around; + align-items: center; + font-size: 1.3rem; + font-weight: 600; + border-radius: 0.8rem; + text-transform: capitalize; +} +.menu ul li i { + font-size: 1.4rem; +} +.menu ul li span { + font-size: 1.2rem; +} +.menu ul li:hover { + background-color: rgba(128, 128, 128, 0.4); +} +.hide { + display: none; +} +.menu ul a { + text-decoration: none; + color: #31312f; +} @media (max-width: 600px) { nav { height: 14vw; padding: 0 3vw; } nav p { - font-size: 3.2vw; + font-size: 4vw; } nav i { font-size: 5vw; @@ -102,7 +159,7 @@ nav a:hover::before { gap: 2vw; } .info a { - padding: 1vw 2vw; + padding: 2vw 2.5vw; } .info a h4 { display: none; @@ -110,4 +167,13 @@ nav a:hover::before { .info a i { font-size: 4vw; } + .menu { + right: 2vw; + top: 16vw; + } + .profile, + .profile img { + height: 38px; + width: 38px; + } } diff --git a/public/css/newStudent.css b/public/css/newStudent.css index 68f27df..0404eaf 100644 --- a/public/css/newStudent.css +++ b/public/css/newStudent.css @@ -54,7 +54,8 @@ body { display: flex; flex-direction: column; gap: 10px; - max-width: 87vh; + /* max-width: 87vh; */ + width: 55vh; padding: 20px; border-top-right-radius: 20px; border-bottom-right-radius: 20px; @@ -80,7 +81,8 @@ body { position: relative; } -.form label .input { +.form label .input, +.flex label select { background-color: #333; color: #fff; width: 100%; @@ -89,9 +91,14 @@ body { border: 1px solid rgba(105, 105, 105, 0.397); border-radius: 10px; } +.flex label select { + width: 95px; + font-size: 1.2em; + padding: 16px 05px 05px 10px; +} .form label .input + span { - color: rgba(255, 255, 255, 0.5); + color: white; position: absolute; left: 10px; top: 0px; @@ -102,14 +109,14 @@ body { .form label .input:placeholder-shown + span { top: 12.5px; - font-size: 0.9em; + font-size: 1.2rem; } .form label .input:focus + span, .form label .input:valid + span { color: #00bfff; top: 0px; - font-size: 0.7em; + font-size: 1em; font-weight: 600; } @@ -132,6 +139,12 @@ body { background-color: #00bfff96; } +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + @keyframes pulse { from { transform: scale(0.9); @@ -148,12 +161,11 @@ body { } .container { display: flex; - margin-left: auto; - margin-right: auto; - width: calc(350px + 87vh); + width: calc(55vh + 65vh); + margin: 5vh auto; } .container img { - width: 87vh; + width: 65vh; border-top-left-radius: 20px; border-bottom-left-radius: 20px; } @@ -166,7 +178,7 @@ body { border-radius: 20px; } .container { - margin: 2vh; + margin: 5vh 2vh; width: auto; } .container img { diff --git a/public/css/show.css b/public/css/show.css index 580f4d6..4a1f8ef 100644 --- a/public/css/show.css +++ b/public/css/show.css @@ -44,6 +44,11 @@ body { margin: 1.5vh; font-size: 3vh; } +.call a, +.remind a { + color: #00bfff; + +} .parent { text-transform: capitalize; } @@ -75,6 +80,9 @@ body { height: 65vh; overflow-y: scroll; } +.feeHistory::-webkit-scrollbar { + display: none; +} .feeHistory h2 { margin-bottom: 3vh; } @@ -128,6 +136,11 @@ body { border-radius: 10px; } +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} .form label .input + span { color: rgba(255, 255, 255, 0.5); position: absolute; @@ -179,6 +192,11 @@ body { .addFess { margin-right: 3vw; } +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} @media (max-width: 600px) { .list { flex-direction: column; diff --git a/public/css/signLogin.css b/public/css/signLogin.css new file mode 100644 index 0000000..3e82538 --- /dev/null +++ b/public/css/signLogin.css @@ -0,0 +1,167 @@ +* { + padding: 0; + margin: 0; + box-sizing: border-box; +} +html, +body { + background-color: #f3efea; + scroll-behavior: smooth; + height: 100%; + width: 100%; +} +.main { + position: relative; + width: 100%; + background-color: #f3efea; + position: relative; +} +.container { + display: flex; + justify-content: center; + align-items: center; + height: 87vh; +} +.form { + display: flex; + flex-direction: column; + /* gap: 18px; */ + width: 25rem; + border-radius: 20px; + position: relative; + background-color: #31312f; + color: #fff; + padding: 2.5rem; +} +label { + margin-bottom: 18px; +} +.login { + font-size: 2.5rem; + font-weight: 700; + position: relative; + display: flex; + align-items: center; + padding-left: 30px; + color: #00bfff; + margin-bottom: 18px; +} +.login::before { + width: 18px; + height: 18px; +} +.login::after { + width: 18px; + height: 18px; + animation: pulse 2s linear infinite; +} +.login::before, +.login::after { + position: absolute; + content: ""; + height: 16px; + width: 16px; + border-radius: 50%; + left: 0px; + background-color: #00bfff; +} +.form label { + position: relative; +} +.form label .input, +.flex label select { + background-color: #333; + color: #fff; + width: 100%; + padding: 20px 05px 05px 10px; + outline: 0; + border: 1px solid rgba(105, 105, 105, 0.397); + border-radius: 10px; +} +.flex label select { + width: 95px; + font-size: 1.2em; + padding: 16px 05px 05px 10px; +} +.form label .input + span { + color: white; + position: absolute; + left: 10px; + top: 0px; + font-size: 0.9em; + cursor: text; + transition: 0.3s ease; +} +.form label .input:placeholder-shown + span { + top: 12.5px; + font-size: 1.2rem; +} +.form label .input:focus + span, +.form label .input:valid + span { + color: #00bfff; + top: 0px; + font-size: 1em; + font-weight: 600; +} +.input { + font-size: medium; +} +.submit { + border: none; + outline: none; + padding: 10px; + border-radius: 10px; + color: #fff; + font-size: 1.2rem; + font-weight: 600; + transform: 0.3s ease; + background-color: #00bfff; +} +.submit:hover { + background-color: #00bfff96; +} +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} +/* Signup */ +.signup { + text-align: center; + margin: 1rem 0; + font-size: 0.9rem; + color: #94a3b8; +} +.signup a { + color: #00bfff; + text-decoration: none; +} +.signup a:hover { + text-decoration: underline; +} +/* Divider */ +.divider { + height: 2px; + background: linear-gradient(to right, #31312f, #00bfff, #31312f); + margin-bottom: 1.5rem; +} +@keyframes pulse { + from { + transform: scale(0.9); + opacity: 1; + } + + to { + transform: scale(1.8); + opacity: 0; + } +} +.input_field { + appearance: textfield; +} + +@media (max-width: 600px) { + .login { + font-size: 2rem; + } +} diff --git a/public/css/students.css b/public/css/students.css index a07362d..a85d376 100644 --- a/public/css/students.css +++ b/public/css/students.css @@ -1,8 +1,3 @@ -@font-face { - font-family: font; - src: url(../MethanerseFreeTrial-0WpE4.otf); -} - * { padding: 0; margin: 0; @@ -21,11 +16,10 @@ body { /* min-height: 100vh; */ width: 100%; background-color: #f3efea; - position: relative; } .container { - width: calc(100% - 10vw); - margin: 4vh 5vw; + width: calc(100% - 6vw); + margin: 4vh 3vw; font-family: sans-serif; } .nav-tabs { @@ -37,17 +31,38 @@ body { flex-wrap: wrap; align-items: center; } + +.quick-actions { + display: flex; +} +.quick-actions label { + padding: 1vw 2vw; + border: 1px solid #31312f; + border-radius: 2rem; +} +.quick-actions label input { + border: none; + background-color: #f3efea; + font-size: 16.8px; +} +.quick-actions label input:focus { + outline: none; +} .quick-actions button { padding: 1vw 2vw; border: 1px solid #31312f; background-color: #f3efea; - border-radius: 2rem; + position: relative; + border-radius: 50px; font-weight: 600; cursor: pointer; - margin: 0 1vh; - position: relative; transition: all ease 0.3s; overflow: hidden; + display: flex; + align-items: center; +} +.quick-actions button { + margin: 0 0 0 1vw; } .quick-actions button:hover { transform: translateY(-3px); @@ -59,6 +74,14 @@ body { z-index: 9; position: relative; } +.quick-actions button i { + z-index: 9; +} +.quick-actions button i, +.quick-actions label i { + font-size: 16px; + margin-right: 0.5vw; +} .quick-actions button::before { content: ""; position: absolute; @@ -74,7 +97,8 @@ body { bottom: 0; border-radius: 0; } -.quick-actions button:hover p { +.quick-actions button:hover p, +.quick-actions button:hover i { color: #f3eaea; } .students { @@ -103,9 +127,13 @@ body { .list .details { margin-left: 1vw; } -.list .details h2 { +.details span { + font-size: 1.1rem; +} +.list .details p { overflow: hidden; font-size: clamp(12px, 2vw, 24px); + font-weight: 700; text-transform: capitalize; } .normal { @@ -113,11 +141,11 @@ body { } .yellow { - color: #ff9800; /* Orange/Yellow - pending fee less than 2 months*/ + color: orange; /* Orange/Yellow - pending fee less than 2 months*/ } .red { - color: #bd3026; /* Red - pending fee more than 2 months*/ + color: orangered; /* Red - pending fee more than 2 months*/ } .none { display: none; @@ -169,13 +197,27 @@ body { margin-bottom: 2vw; align-items: center; } + .nav-tabs .filter { + margin-bottom: 2vw; + } + .nav-tabs .filter a { + padding: 2vw 2.5vw; + } .nav-tabs h1 { font-size: 6vw; margin-bottom: 2vw; } + .quick-actions button i, + .quick-actions label i, + .quick-actions label input { + font-size: 4vw; + } + .quick-actions button p { + display: none; + } .quick-actions button { - padding: 1vw 2vw; - margin: 0 1vw; + padding: 2vw 2.5vw; + margin: 0 2vw; } .list { width: 45vw; @@ -188,7 +230,7 @@ body { .list .details { margin-left: 1.5vw; } - .list .details h2 { + .list .details p { font-size: 1.2rem; } .radio-inputs { diff --git a/public/css/style.css b/public/css/style.css index c67ef85..1534b80 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,8 +1,3 @@ -@font-face { - font-family: font; - src: url(../MethanerseFreeTrial-0WpE4.otf); -} - * { padding: 0; margin: 0; diff --git a/public/home.png b/public/home.png new file mode 100644 index 0000000..3fa6ae1 Binary files /dev/null and b/public/home.png differ diff --git a/public/js/filter.js b/public/js/filter.js index 061c4df..0884086 100644 --- a/public/js/filter.js +++ b/public/js/filter.js @@ -1,14 +1,36 @@ document.querySelectorAll('input[name="radio"]').forEach((radio) => { radio.addEventListener("change", function () { - const selected = this.value; // selected grade - const studentss = document.querySelectorAll(".students .list"); - - studentss.forEach((students) => { - students.style.display = "block"; - const grade = students.dataset.grade; - if (!(grade === selected)) { - students.style.display = "none"; + const selected = this.value; + const students = document.querySelectorAll(".students .list"); + students.forEach((student) => { + const grade = student.dataset.grade; + if (selected === "all" || grade === selected) { + student.style.display = "block"; + } else { + student.style.display = "none"; } + updateCount(); }); }); }); + +function updateCount() { + const visible = document.querySelectorAll( + ".students .list:not([style*='display: none'])" + ).length; + document.querySelector( + "#student-count" + ).textContent = `Total students: ${visible}`; +} + +document.querySelector("#searchBox").addEventListener("input", function () { + const search = this.value.toLowerCase(); + const students = document.querySelectorAll(".students .list"); + + students.forEach((student) => { + const name = student.querySelector(".details p").textContent.toLowerCase(); + student.style.display = name.includes(search) ? "block" : "none"; + }); + + updateCount(); +}); diff --git a/public/js/newStudent.js b/public/js/newStudent.js new file mode 100644 index 0000000..93c64bb --- /dev/null +++ b/public/js/newStudent.js @@ -0,0 +1,26 @@ +const gradeFees = { + Grade: 250, + 0: 250, + "1st": 250, + "2nd": 250, + "3rd": 250, + "4th": 300, + "5th": 300, + "6th": 400, + "7th": 400, + "8th": 500, + "9th": 600, + "10th": 700, + "11th": 700, + "12th": 700, +}; +const gradeSelect = document.getElementById("grade"); +const feesInput = document.getElementById("fees"); +gradeSelect.addEventListener("change", function () { + const selectedGrade = this.value; + if (gradeFees[selectedGrade]) { + feesInput.value = gradeFees[selectedGrade]; // auto-fill fee + } else { + feesInput.value = ""; // clear if no match + } +}); diff --git a/public/pic2.png b/public/pic2.png new file mode 100644 index 0000000..d23c158 Binary files /dev/null and b/public/pic2.png differ diff --git a/public/view.png b/public/view.png new file mode 100644 index 0000000..61c021b Binary files /dev/null and b/public/view.png differ diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..d192e3f --- /dev/null +++ b/readme.md @@ -0,0 +1,132 @@ +# πThe Dynamic Vision - Student Management System + +Live link : https://the-dynamic-vision.onrender.com/home \ +A complete web-based student management system for coaching centers. +Built with **Node.js, Express, MongoDB, and EJS**. + +> Designed to automate student records, payment tracking, and monthly fees β with GitHub Actions automation. + +## Project Overview + +##  + +##  + +## π Features + +- **π Student Management** : Add, edit, delete student profiles, Track classes, parent details, contact info, Auto-manage monthly fee status,View all students in a clean dashboard. + +- **π³ Smart Payment Tracking** : Add payments with date and amount, Automatic due calculation, Full payment history. + +- **π Automation** : Automatically adds monthly due fees, Runs using **GitHub Actions**, Secure environment variables through **GitHub secrets**. +- **π Dashboard & UI** : Built with EJS templates,Responsive frontend,Clean layout for managing students. +- **π οΈ Admin Tools** : Add/remove students,Edit details,View fee status,Manage payment records. +- **π¬ SMS/WhatsApp fee reminders** : Monthly fees reminder. + +## **π Project Structure** + +
+βββ .github +β βββ workflows +βββ controllers β Request handlers +βββ middleware β Custom middleware +βββ models β Database schemas +βββ public/ β Static assets +β βββ css +β βββ img +β βββ js +βββ routes β Express routes +βββ services +βββ utils β Helper functions +βββ views/ β EJS templates +β βββ includes +β βββ layouts +β βββ listings +βββ .env +βββ .gitignore +βββ index.js +βββ package.json +βββ package-lock.json + ++ +## π οΈ Tech Stack + +Mobile-friendly PWA version +| Technology | Purpose | +| ----------------------- | --------------------------- | +| **Node.js** | Backend runtime | +| **Express.js** | Server framework | +| **MongoDB + Mongoose** | Database & ORM | +| **EJS Template Engine** | UI rendering | +| **GitHub Actions** | Free automated monthly fees | + +## πΌοΈ Screenshot + +##  + +##  + +## βοΈ Installation & Setup + +Follow these steps to set up and run the project locally: + +1. **Clone the Repository:** + + ```bash + git clone https://github.com/vikrant-vikrant/Dynamic-Vision + cd DynamicVision + ``` + +2. **Install Dependencies:** + + ```bash + npm install + ``` + +3. **Set Up Environment Variables:** + + Configure the following environment variables by creating a .env file in the root of your project: + + Example :- + + ```bash + + #https://www.mongodb.com/ (MongoDb Atlas) (Change key) + ATLASDB_URL=mongodb+srv://demo:kL089dndd@cluster0.kkdnvkdkds.mongodb.net/?retryWrites=true&w=majority + + #Add Random Secret Key + SECRET=IamHereToHelpYou + ``` + + Replace the values with your specific configurations. + +4. **Run the Application:** + + ```bash + node index.js + ``` + +5. **Open in Your Browser:** + + Open `http://localhost:8000/home` in your web browser. + +## Future Enhancements + +- Student attendance system + +- Teacher login panel + +- Admin analytics dashboard + +## Author + +VIKRANT \ +LinkedIn : https://www.linkedin.com/in/vikrant-vikrant-0a58b636b/ + +## Thank You + +Thank you for exploring Student Management System! Your feedback is valuable. If you have any suggestions or thoughts, feel free to share them with us. π \ +If you find this project helpful, donβt forget to β star the repository! + +--- diff --git a/routes/fund.js b/routes/fund.js new file mode 100644 index 0000000..a43ff75 --- /dev/null +++ b/routes/fund.js @@ -0,0 +1,6 @@ +const express = require("express"); +const router = express.Router(); +const fund = require("../controllers/fund"); +const { isLoggedIn } = require("../middleware/isLoggedIn"); +router.route("/fund").get(isLoggedIn, fund.fund); +router.route("/fund/expenses").post(isLoggedIn, fund.addExpense); diff --git a/routes/students.js b/routes/students.js index 67d1f2a..37bf386 100644 --- a/routes/students.js +++ b/routes/students.js @@ -1,18 +1,20 @@ const express = require("express"); const router = express.Router(); -const studentControllers = require("../controllers/students"); -router.route("/").get(studentControllers.students); +const student = require("../controllers/students"); +const validateObjectId = require("../middleware/validateObjectId"); +const { isLoggedIn } = require("../middleware/isLoggedIn"); +router.route("/").get(isLoggedIn, student.students); router .route("/newStudent") - .get(studentControllers.newStudentForm) - .post(studentControllers.addNewStudent); + .get(isLoggedIn, student.newStudentForm) + .post(isLoggedIn, student.addNewStudent); router .route("/:id") - .get(studentControllers.showStudent) - .delete(studentControllers.deleteStudent); + .get(isLoggedIn, validateObjectId, student.showStudent) + .delete(isLoggedIn, validateObjectId, student.deleteStudent); router .route("/:id/edit") - .get(studentControllers.editStudent) - .put(studentControllers.saveEditStudent); -router.post("/:id/addFees", studentControllers.addFees); + .get(isLoggedIn, validateObjectId, student.editStudent) + .put(isLoggedIn, validateObjectId, student.saveEditStudent); +router.post("/:id/addFees", isLoggedIn, student.addFees); module.exports = router; diff --git a/routes/user.js b/routes/user.js new file mode 100644 index 0000000..9eb34fa --- /dev/null +++ b/routes/user.js @@ -0,0 +1,17 @@ +const express = require("express"); +const router = express.Router(); +const wrapAsync = require("../utils/catchAsync"); +const userController = require("../controllers/user.js"); + +router + .route("/signup") + .get(userController.renderSignupForm) + .post(wrapAsync(userController.signup)); + +router + .route("/login") + .get(userController.renderloginForm) + .post(wrapAsync(userController.login)); + +router.get("/logout", userController.logout); +module.exports = router; diff --git a/services/cronJobs.js b/services/cronJobs.js new file mode 100644 index 0000000..8830bb0 --- /dev/null +++ b/services/cronJobs.js @@ -0,0 +1,49 @@ +require("dotenv").config({ path: require("path").join(__dirname, "../.env") }); +const mongoose = require("mongoose"); +const Student = require("../models/students"); + +const MONGO_URL = process.env.MONGO_URL; +async function connectDB() { + try { + await mongoose.connect(MONGO_URL); + console.log("β Connected to MongoDB"); + } catch (err) { + console.error("β MongoDB connection error:", err); + process.exit(1); + } +} +async function addMonthlyDueFees() { + try { + const students = await Student.find(); + const today = new Date(); + for (let s of students) { + if (!s.joiningDate) continue; + if (today.getDate() !== s.joiningDate.getDate()) continue; + const last = s.lastDueAdded; + if ( + !last || + last.getMonth() !== today.getMonth() || + last.getFullYear() !== today.getFullYear() + ) { + s.dueFees = (s.dueFees || 0) + (s.fees || 0); + s.lastDueAdded = today; + await s.save(); + console.log(`β Added due for ${s.name}`); + } else { + console.log(`β οΈ Already added fees for ${s.name} this month`); + } + } + console.log("π Monthly due update completed!"); + } catch (err) { + console.error("Auto-due job error:", err); + } finally { + await mongoose.disconnect(); + console.log("π MongoDB disconnected"); + process.exit(0); + } +} +async function main() { + await connectDB(); + await addMonthlyDueFees(); +} +main(); diff --git a/utils/dateUtils.js b/utils/dateUtils.js new file mode 100644 index 0000000..4367af9 --- /dev/null +++ b/utils/dateUtils.js @@ -0,0 +1,5 @@ +function getTodayDate() { + return new Date().toISOString().split("T")[0]; // YYYY-MM-DD +} + +module.exports = { getTodayDate }; diff --git a/utils/jwt.js b/utils/jwt.js new file mode 100644 index 0000000..65b5316 --- /dev/null +++ b/utils/jwt.js @@ -0,0 +1,5 @@ +const jwt = require("jsonwebtoken"); + +exports.signToken = (userId) => { + return jwt.sign({ id: userId }, process.env.JWT_SECRET, { expiresIn: "7d" }); +}; diff --git a/views/includes/flash.ejs b/views/includes/flash.ejs new file mode 100644 index 0000000..944601b --- /dev/null +++ b/views/includes/flash.ejs @@ -0,0 +1,34 @@ + +<% if(success && success.length){ %> +
GRADE : <%= students.grade %>
-- DUE FEES : <%= students.dueFees %> -
-No fees recorded this month.
+ <% } %> +