From e4f3de80fa02d336583510d3b4478e2c3d0b6cbb Mon Sep 17 00:00:00 2001 From: Harsh Gupta Date: Thu, 30 Apr 2026 19:02:16 +0530 Subject: [PATCH] api created for employees,authentication and attendance. --- Learnings.md | 5 + server/config/db.js | 4 +- server/constants/departments.js | 1 + server/controllers/attendanceController.js | 113 +++++++++++++++ server/controllers/authController.js | 103 +++++++++++++ server/controllers/employeeController.js | 159 +++++++++++++++++++++ server/controllers/profileController.js | 57 ++++++++ server/middleware/auth.js | 35 +++++ server/models/Attendance.js | 46 ++++++ server/models/Employee.js | 71 +++++++++ server/models/User.js | 24 ++++ server/routes/AttendanceRoutes.js | 10 ++ server/routes/authRoutes.js | 14 ++ server/routes/employeeRoutes.js | 15 ++ server/routes/profileRoutes.js | 10 ++ server/server.js | 13 +- 16 files changed, 676 insertions(+), 4 deletions(-) create mode 100644 server/constants/departments.js create mode 100644 server/controllers/attendanceController.js create mode 100644 server/controllers/authController.js create mode 100644 server/controllers/employeeController.js create mode 100644 server/controllers/profileController.js create mode 100644 server/middleware/auth.js create mode 100644 server/models/Attendance.js create mode 100644 server/models/Employee.js create mode 100644 server/models/User.js create mode 100644 server/routes/AttendanceRoutes.js create mode 100644 server/routes/authRoutes.js create mode 100644 server/routes/employeeRoutes.js create mode 100644 server/routes/profileRoutes.js diff --git a/Learnings.md b/Learnings.md index e69de29..0d874e3 100644 --- a/Learnings.md +++ b/Learnings.md @@ -0,0 +1,5 @@ +-->claudinary ,express-upload ,multer + +-->Make sure to include type:module + +-->correct file name extensions i.e .js .ts etc \ No newline at end of file diff --git a/server/config/db.js b/server/config/db.js index 17a8c0d..c44c6b0 100644 --- a/server/config/db.js +++ b/server/config/db.js @@ -1,9 +1,9 @@ -import mongoose, { connect } from "mongoose"; +import mongoose from "mongoose"; export const connectDb = async () => { try { - await mongoose.connect(MONGODB_URI); + await mongoose.connect(process.env.MONGODB_URI); console.log("connect to database") } catch (error) { console.log(error); diff --git a/server/constants/departments.js b/server/constants/departments.js new file mode 100644 index 0000000..715546f --- /dev/null +++ b/server/constants/departments.js @@ -0,0 +1 @@ +export const DEPARTMENTS = ["Engineering", "Human Resources", "Marketing", "Sales", "Finance", "Operations", "IT Support", "Customer Success", "Product Management", "Design"]; \ No newline at end of file diff --git a/server/controllers/attendanceController.js b/server/controllers/attendanceController.js new file mode 100644 index 0000000..51a0f5b --- /dev/null +++ b/server/controllers/attendanceController.js @@ -0,0 +1,113 @@ +import Attendance from "../models/Attendance.js"; +import { Employee } from "../models/Employee.js" + + +//clock in/out for employee +//POST api/attendance + +export const clockInOut = async (req, res) => { + + try { + const session = req.session; + + const employee = await Employee.findOne({ userId: session.userId }); + + if (!employee) + return res.status(404).json({ message: "Employee not found" }) + + + if (employee.isDeleted) + return res.status(403).json({ message: "Your account id deactivated.You cannot clock in/out" }); + + + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const existing = await getAttendance.findOne({ + employeeId: employee._id, + date: today + }) + + const now = new Date(); + + if (!existing) { + const isLate = now.getHours() >= 9 && now.getMinutes() > 0; + + const attendance = await Attendance.create({ + employeeId: employee._id, + date: today, + checkIn: now, + status: isLate ? "LATE" : "PRESENT", + }) + + return res.json({ + success: true, + type: "CHECK_IN", + data: attendance + }); + } else if (!existing.checkOut) { + const checkInTime = new Date(existing.checkIn).getTime() + const diffMs = now.getTime() - checkInTime; + const diffHours = diffMs / (1000 * 60 * 60); + + existing.checkOut = now; + + const workingHours = parseFloat(diffHours.toFixed(2)); + + let dayType = "Half Day"; + if (workingHours >= 8) + dayType = "Full Day"; + else if (workingHours >= 6) + dayType = "Three Quarter Day"; + else if (workingHours >= 4) + dayType = "Half Day"; + else + dayType = "Short Day"; + + + await existing.save(); + return res.json({ + success: true, + type: "CHECK_OUT", + data: existing + }) + } else { + return res.json({ + success: true, + type: "CHECK_OUT", + data: existing + }) + } + } catch (error) { + console.log(error); + console.error("Attendance Error:", error); + return res.status(500).json({ error: "Operation failed" }); + } +} + + +//Get attendance for employee +//GET /api/attendance + +export const getAttendance = async (req, res) => { + try { + const session = req.session; + const employee = await Employee.findOne({ userId: session.userId }); + + if (!employee) + return res.status(404).json({ message: "Employee not found" }) + + const limit = parseInt(req.query.limit || 30); + const history = await Attendance.find({ employeeId: employee._id }).sort({ date: -1 }).limit(limit) + + + return res.json({ + data: history, + employee: { isDeleted: employee.isDeleted } + }) + + } catch (error) { + return res.status(500).json({ error: "Failed to Fetch Attendance" }); + } +} \ No newline at end of file diff --git a/server/controllers/authController.js b/server/controllers/authController.js new file mode 100644 index 0000000..e48d733 --- /dev/null +++ b/server/controllers/authController.js @@ -0,0 +1,103 @@ +import User from "../models/User"; +import bcrypt from "bcryptjs"; +import jwt from "jsonwebtoken"; + + +//Login for employee and admin +//POST /api/auth/login + +export const login = async (req, res) => { + try { + const { email, password, role_type } = req.body; + + if (!email || !password) { + return res.status(400).json({ message: "Please provide email and password" }) + } + + const user = await User.findOne({ email }); + + if (!user) { + return res.status(401).json({ message: "Invalid email or password" }) + } + + if (role_type === "admin" && user.role !== "ADMIN") { + return res.status(401).json({ message: "Not authorized as admin" }) + } + + if (role_type === "employee" && user.role !== "EMPLOYEE") { + return res.status(401).json({ message: "Not authorized as employee" }) + } + + const isValid = await bcrypt.compare(password, user.password); + + if (!isValid) { + return res.status(401).json({ message: "Invalid email or password" }) + } + + + const payload = { + userId: user._id.toString(), + role: user.role, + email: user.email, + } + + const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: "7d" }); + + return res.json({ user: payload, token }) + } catch (err) { + console.log("Login Error:", err); + return res.status(500).json({ message: "Failed to login" }); + } +} + + +//Get session for employee and admin +//GET /api/auth/session + + +export const session = (req, res) => { + const session = req.session; + + if (!req.session) return res.status(401).json({ message: "No session" }); + + return res.json({ user: session }); +} + + +//change password for employee and admin +//POST /api/auth/change-password + +export const changePassword = async (req, res) => { + try { + const userSession = req.session; + const { currentPassword, newPassword } = req.body; + + if (!userSession || !userSession.userId) + return res.status(401).json({ message: "Unauthorized" }); + + + if (!currentPassword || !newPassword) + return res.status(400).json({ message: "Both passwords field required" }) + + const user = await User.findById(userSession.userId); + + if (!user) + return res.status(404).json({ error: "User not found" }); + + + const isValid = await bcrypt.compare(currentPassword, user.password); + + if (!isValid) + return res.status(400).json({ error: "Current password is incorrect" }); + + const hashed = await bcrypt.hash(newPassword, 10); + + await User.findByIdAndUpdate(userSession.userId, { password: hashed }); + + + return res.json({ success: true }) + + } catch (error) { + return res.status(500).json({ error: "Failed to change password" }) + } +} \ No newline at end of file diff --git a/server/controllers/employeeController.js b/server/controllers/employeeController.js new file mode 100644 index 0000000..74c0163 --- /dev/null +++ b/server/controllers/employeeController.js @@ -0,0 +1,159 @@ +import Employee from "../models/Employee.js"; +import bcrypt from "bcryptjs"; +import User from "../models/User.js"; + + +// Get employees +// GET /api/employees +export const getEmployee = async (req, res) => { + + try { + const { department } = req.query; + + const where = {}; + + if (department) + where.department = department; + + + const employees = await Employee.find(where) + .sort({ createdAt: -1 }) + .populate("userId", "email role") + .lean(); + + + const result = employees.map((emp) => ({ + ...emp, + id: emp._id.toString(), + user: emp.userId ? { email: emp.userId.email, role: emp.userId.role } : null + })) + + return res.json(result) + } catch (error) { + return res.status(500).json({ message: "failed to fetch employees" }) + } +} + +// Create employee +// POST /api/employees +export const createEmployee = async (req, res) => { + try { + + const { firstName, lastName, email, phone, position, department, basicSalary, allowances, deductions, joinDate, password, role, bio } = req.bod; + + + if (!email || !password || !firstName || !lastName) + return res.status(400).json({ message: "Missing required field" }); + + const hashed = await bcrypt.hash(password, 10); + + const user = await User.create({ + email, + password: hashed, + role: role || "EMPLOYEE" + }); + + + const employee = await Employee.create({ + userId: user._id, + firstName, + lastName, + phone, + position, + department: department || "Engineering", + basicSalary: Number(basicSalary) || 0, + allowances: Number(allowances) || 0, + deductions: Number(deductions) || 0, + joinDate: new Date(joinDate), + bio: bio || "", + }) + + return res.status(201).json({ + message: "Employee created successfully", + }) + + } catch (error) { + if (error.code === 11000) + return res.status(400).json({ error: "Email already exists" }); + + console.error("Create employee error:", error); + return res.status(500).json({ message: "Failed to create employee" }); + } +} + + + +// update employee +// PUT /api/employees/:id +export const updateEmployee = async (req, res) => { + try { + const { id } = req.params; + const { firstName, lastName, email, phone, position, department, basicSalary, allowances, deductions, password, role, bio, employmentStatus } = req.bod; + + + const employee = await Employee.findById(id); + + + if (!employee) + return res.status(404).json({ error: "Employee not found " }); + + + await Employee.findByIdAndUpdate(id, { + firstName, + lastName, + phone, + position, + department: department || "Engineering", + basicSalary: Number(basicSalary) || 0, + allowances: Number(allowances) || 0, + deductions: Number(deductions) || 0, + joinDate: new Date(joinDate), + employmentStatus: employmentStatus || "ACTIVE", + bio: bio || "", + }) + + + //update user record + const userUpdate = { email } + + if (role) + userUpdate.role = role; + + if (password) + userUpdate.password = await bcrypt.hash(password, 10) + + await User.findByIdAndUpdate(employee.userId, userUpdate); + + return res.json({ success: true }); + } catch (error) { + if (error.code === 11000) + return res.status(400).json({ error: "Email already exists" }); + + return res.status(500).json({ message: "Failed to update employee" }); + } +} + +// delete employee +// DELETE /api/employees/:id +export const deleteEmployee = async (req, res) => { + try { + const {id}=req.params; + + const employee=await Employee.findById(id); + + if(!employee) + return res.status(404).json({error:"Employee not found"}); + + + employee.isDeleted=true; + employee.employmentStatus="INACTIVE"; + + await employee.save(); + + + return res.json({success:true}); + + } catch (error) { + return res.status(500).json({ message: "Failed to delete employee" }); + } +} \ No newline at end of file diff --git a/server/controllers/profileController.js b/server/controllers/profileController.js new file mode 100644 index 0000000..29a6c66 --- /dev/null +++ b/server/controllers/profileController.js @@ -0,0 +1,57 @@ +import Employee from "../models/Employee.js"; + +//GET profile +//GET /api/profile + + +export const getProfile = async (req, res) => { + try { + + const session = req.session; + const employee = await Employee.findOne({ userId: session.userId }).populate("userId", "email role") + + if (!employee) { + //Authenticated user is not an employee-return admin profile + + return res.json({ + firstName: "Admin", + lastName: "", + email: session.email, + }) + } + + return res.json(employee); + } catch (error) { + return res.status(500).json({ error: "Failed to fetch profile" }); + } +} + + + +//UPDATE profile +//PUT /api/profile + +export const updateProfile = async (req, res) => { + try { + const sesison = req.session; + const employee = await Employee.findOne({ userId: sesison.userId }) + + if (!employee) { + return res.status(404).json({ error: "Employee not found" }) + } + + if (employee.isDeleted) { + return res.status(403).json({ error: "Your Account is deactivated.You cannot update your profile" }); + } + + + await Employee.findByIdAndUpdate(employee._id, { + bio: req.body.bio + }) + + return res.json({ success: true }); + + } catch (error) { + return res.status(500).json({ error: "Failed to update profile" }) + } +} \ No newline at end of file diff --git a/server/middleware/auth.js b/server/middleware/auth.js new file mode 100644 index 0000000..80a1812 --- /dev/null +++ b/server/middleware/auth.js @@ -0,0 +1,35 @@ +import { jwt } from "jsonwebtoken"; + +export const protect = (req, res, next) => { + try { + const authHeader = req.headers.authorization; + + + if (!authHeader || !authHeader.startsWith("Bearer")) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const token = authHeader.split(" ")[1]; + + const session = jwt.verify(token, process.env.JWT_SECRET); + + + if (!session) + return res.status(401).json({ error: "Unauthorized" }); + + req.session = session; + + next(); + } catch (error) { + res.status(401).json({ error: "Unauthorized" }); + } +} + + +export const adminProtect = (req, res, next) => { + if (!req?.session?.role !== "ADMIN") { + return res.status(401).json({ error: "Admin access required" }) + } + + next(); +} \ No newline at end of file diff --git a/server/models/Attendance.js b/server/models/Attendance.js new file mode 100644 index 0000000..3aa873b --- /dev/null +++ b/server/models/Attendance.js @@ -0,0 +1,46 @@ +import mongoose, { model } from "mongoose"; + + +const AttendanceSchema = new mongoose.Schema({ + employeeId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Employee", + required: true, + unique: true + }, + date: { + type: Date, + required: true, + }, + checkIn: { + type: Date, + default: null + }, + checkOut: { + type: Date, + default: null + }, + status: { + type: String, + enum: ["PRESENT", "ABSENT", "LATE"], + default: "PRESENT" + }, + workingHours: { + type: Number, + default: null + }, + dayType: { + type: String, + enum: ["Full Day", "Three Quarter Day", "Half Day", "Short Day", null], + default: null + } +}, + { timestamps: true }); + + +AttendanceSchema.index({ employeeId: 1, date: 1 }, + { unique: true } +); + +const Attendance = mongoose.model("Attendance", AttendanceSchema); +export default Attendance; \ No newline at end of file diff --git a/server/models/Employee.js b/server/models/Employee.js new file mode 100644 index 0000000..4c6ef94 --- /dev/null +++ b/server/models/Employee.js @@ -0,0 +1,71 @@ +import mongoose from "mongoose"; +import { DEPARTMENTS } from "../constants/departments.js"; + + +const EmployeeSchema = new mongoose.Schema({ + userId: { + type: mongoose.Schema.types.ObjectId, + ref: "User", + required: true, + unique: true + }, + firstName: { + type: String, + required: true, + }, + lastName: { + type: String, + required: true, + }, + email: { + type: String, + required: true, + unique: true + }, + phone: { + type: String, + required: true, + }, + position: { + type: String, + required: true, + }, + basicSalary: { + type: Number, + required: true, + }, + allowances: { + type: Number, + default: 0, + }, + deductions: { + type: Number, + default: 0, + }, + employmentStatus: { + type: String, + enum: ["ACTIVE", "INACTIVE"], + default: "ACTIVE" + }, + joinDate: { + type: Date, + required: true, + }, + isDeleted: { + type: Boolean, + default: false, + }, + bio: { + type: String, + default: "" + }, + department: { + type: String, + enum:DEPARTMENTS + } +}, + { timestamps: true }); + + +const Employee = mongoose.model("Employee", EmployeeSchema); +export default Employee; \ No newline at end of file diff --git a/server/models/User.js b/server/models/User.js new file mode 100644 index 0000000..039cc6c --- /dev/null +++ b/server/models/User.js @@ -0,0 +1,24 @@ +import mongoose, { model } from "mongoose"; + + +const UserSchema = new mongoose.Schema({ + email: { + type: String, + required: true, + unique: true + }, + password: { + type: String, + required: true + }, + role: { + type: String, + enum: ["ADMIN", "EMPLOYEE"], + dafault: "EMPLOYEE" + } +}, + { timestamps: true }); + + +const User = mongoose.model("User", UserSchema); +export default User; \ No newline at end of file diff --git a/server/routes/AttendanceRoutes.js b/server/routes/AttendanceRoutes.js new file mode 100644 index 0000000..dbd75ff --- /dev/null +++ b/server/routes/AttendanceRoutes.js @@ -0,0 +1,10 @@ +import { Router } from "express"; +import { clockInOut, getAttendance } from "../controllers/attendanceController.js"; +import { protect } from "../middlewares/auth.js"; + +const attendanceRouter = Router(); + +attendanceRouter.post("/", protect, clockInOut); +attendanceRouter.get("/", protect, getAttendance); + +export default attendanceRouter; \ No newline at end of file diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js new file mode 100644 index 0000000..dd3dda6 --- /dev/null +++ b/server/routes/authRoutes.js @@ -0,0 +1,14 @@ +import {Router} from "express"; + +import {protect} from "../middleware/auth.js"; + +import {login,session,changePassword} from "../controllers/authController.js"; + + +const authRouter=Router(); + +authRouter.post("/login",login); +authRouter.get("/session",protect,session); +authRouter.post("/change-password",protect,changePassword); + +export default authRouter; \ No newline at end of file diff --git a/server/routes/employeeRoutes.js b/server/routes/employeeRoutes.js new file mode 100644 index 0000000..98a62a5 --- /dev/null +++ b/server/routes/employeeRoutes.js @@ -0,0 +1,15 @@ +import { Router } from "express"; + +import { protect, adminProtect } from "../middleware/auth.js"; + +import { getEmployee, updateEmployee, createEmployee, deleteEmployee } from "../controllers/employeeController.js"; + + +const exmployeesRouter = Router(); + +exmployeesRouter.get("/",protect,adminProtect,getEmployee); +exmployeesRouter.post("/",protect,adminProtect,createEmployee); +exmployeesRouter.put("/:id",protect,adminProtect,updateEmployee); +exmployeesRouter.delete("/:id",protect,adminProtect,deleteEmployee); + +export default exmployeesRouter; \ No newline at end of file diff --git a/server/routes/profileRoutes.js b/server/routes/profileRoutes.js new file mode 100644 index 0000000..a10a10d --- /dev/null +++ b/server/routes/profileRoutes.js @@ -0,0 +1,10 @@ +import {Router} from "express"; +import { protect } from "../middleware/auth.js"; +import { getProfile,updateProfile } from "../controllers/profileController.js"; + +const profileRouter=Router(); + +profileRouter.get("/",protect,getProfile); +profileRouter.put("/",protect,updateProfile); + +export default profileRouter; \ No newline at end of file diff --git a/server/server.js b/server/server.js index 3bf64f4..e1d0088 100644 --- a/server/server.js +++ b/server/server.js @@ -1,8 +1,14 @@ +import "dotenv/config"; import express from "express" import cors from "cors"; import multer from "multer"; import { connectDb } from "./config/db.js"; +import exmployeesRouter from "./routes/employeeRoutes.js"; +import authRouter from "./routes/authRoutes.js"; +import profileRouter from "./routes/profileRoutes.js"; +import attendanceRouter from "./routes/AttendanceRoutes.js"; + const app = express() @@ -20,10 +26,13 @@ app.get('/', (req, res) => res.send("server is running") ); - -await connectDb(); +app.use('/api/auth', authRouter); +app.use('/api/employees', exmployeesRouter); +app.use('/api/profile', profileRouter); +app.use('/api/attendance', attendanceRouter); +await connectDb(); app.listen(PORT, () => { console.log(`server is running on port no ${PORT}`)