commit 521a1fea79f432558b90acd6abd28157ea259ca8 Author: ARJUN S THAMPI <61703062+arjun-thampi@users.noreply.github.com> Date: Thu Mar 12 14:15:44 2026 +0530 feat: basic api setup and boilerplate diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..126419d --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,5 @@ +node_modules +# Keep environment variables out of version control +.env + +/src/generated/prisma diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..9bdadf6 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,35 @@ +{ + "name": "gg-node-backend", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "nodemon src/app.js", + "start": "node src/app.js", + "prisma": "prisma", + "migrate": "npx prisma migrate dev", + "generate": "npx prisma generate" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "module", + "dependencies": { + "@editorjs/editorjs": "^2.31.4", + "@editorjs/header": "^2.8.8", + "@editorjs/list": "^2.0.9", + "@prisma/client": "^6.19.2", + "bcrypt": "^6.0.0", + "bcryptjs": "^3.0.3", + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "express-session": "^1.19.0", + "jsonwebtoken": "^9.0.3", + "multer": "^2.1.1", + "prisma": "^6.19.2" + }, + "devDependencies": { + "nodemon": "^3.1.11" + } +} diff --git a/backend/prisma.config.js b/backend/prisma.config.js new file mode 100644 index 0000000..41b4755 --- /dev/null +++ b/backend/prisma.config.js @@ -0,0 +1,14 @@ +// This file was generated by Prisma, and assumes you have installed the following: +// npm install --save-dev prisma dotenv +import "dotenv/config"; +import {defineConfig} from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: process.env["DATABASE_URL"], + }, +}); diff --git a/backend/prisma/migrations/20260223061336_init/migration.sql b/backend/prisma/migrations/20260223061336_init/migration.sql new file mode 100644 index 0000000..d450a80 --- /dev/null +++ b/backend/prisma/migrations/20260223061336_init/migration.sql @@ -0,0 +1,79 @@ +-- CreateTable +CREATE TABLE "Doctor" ( + "id" SERIAL NOT NULL, + "doctorId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "designation" TEXT, + "workingStatus" TEXT, + "qualification" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Doctor_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Department" ( + "id" SERIAL NOT NULL, + "departmentId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "para1" TEXT, + "para2" TEXT, + "para3" TEXT, + "facilities" TEXT, + "services" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Department_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DoctorDepartment" ( + "id" SERIAL NOT NULL, + "doctorId" INTEGER NOT NULL, + "departmentId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "DoctorDepartment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DoctorTiming" ( + "id" SERIAL NOT NULL, + "doctorDepartmentId" INTEGER NOT NULL, + "monday" TEXT, + "tuesday" TEXT, + "wednesday" TEXT, + "thursday" TEXT, + "friday" TEXT, + "saturday" TEXT, + "sunday" TEXT, + "additional" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "DoctorTiming_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Doctor_doctorId_key" ON "Doctor"("doctorId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Department_departmentId_key" ON "Department"("departmentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "DoctorDepartment_doctorId_departmentId_key" ON "DoctorDepartment"("doctorId", "departmentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "DoctorTiming_doctorDepartmentId_key" ON "DoctorTiming"("doctorDepartmentId"); + +-- AddForeignKey +ALTER TABLE "DoctorDepartment" ADD CONSTRAINT "DoctorDepartment_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "Doctor"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DoctorDepartment" ADD CONSTRAINT "DoctorDepartment_departmentId_fkey" FOREIGN KEY ("departmentId") REFERENCES "Department"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DoctorTiming" ADD CONSTRAINT "DoctorTiming_doctorDepartmentId_fkey" FOREIGN KEY ("doctorDepartmentId") REFERENCES "DoctorDepartment"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20260223105838_add_user/migration.sql b/backend/prisma/migrations/20260223105838_add_user/migration.sql new file mode 100644 index 0000000..6ff09eb --- /dev/null +++ b/backend/prisma/migrations/20260223105838_add_user/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "username" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" TEXT DEFAULT 'admin', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); diff --git a/backend/prisma/migrations/20260311120120_blog/migration.sql b/backend/prisma/migrations/20260311120120_blog/migration.sql new file mode 100644 index 0000000..44d6258 --- /dev/null +++ b/backend/prisma/migrations/20260311120120_blog/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "Blog" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "writer" TEXT, + "image" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "content" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Blog_pkey" PRIMARY KEY ("id") +); diff --git a/backend/prisma/migrations/migration_lock.toml b/backend/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/backend/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma new file mode 100644 index 0000000..506ec5e --- /dev/null +++ b/backend/prisma/schema.prisma @@ -0,0 +1,100 @@ + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id Int @id @default(autoincrement()) + username String @unique + password String + role String? @default("admin") + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Doctor { + id Int @id @default(autoincrement()) + doctorId String @unique + name String + designation String? + workingStatus String? + qualification String? + + departments DoctorDepartment[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + + +model Department { + id Int @id @default(autoincrement()) + departmentId String @unique + name String + + para1 String? + para2 String? + para3 String? + facilities String? + services String? + + doctors DoctorDepartment[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model DoctorDepartment { + id Int @id @default(autoincrement()) + + doctorId Int + departmentId Int + + doctor Doctor @relation(fields: [doctorId], references: [id]) + department Department @relation(fields: [departmentId], references: [id]) + + timing DoctorTiming? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([doctorId, departmentId]) +} + +model DoctorTiming { + id Int @id @default(autoincrement()) + + doctorDepartmentId Int @unique + doctorDepartment DoctorDepartment @relation(fields: [doctorDepartmentId], references: [id]) + + monday String? + tuesday String? + wednesday String? + thursday String? + friday String? + saturday String? + sunday String? + additional String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + + +model Blog { + id Int @id @default(autoincrement()) + title String + writer String? + image String? + content Json + isActive Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} \ No newline at end of file diff --git a/backend/src/app.js b/backend/src/app.js new file mode 100644 index 0000000..ba01c32 --- /dev/null +++ b/backend/src/app.js @@ -0,0 +1,42 @@ +import express from "express"; +import dotenv from "dotenv"; +import cors from "cors"; + +import departmentRoutes from "./routes/department.routes.js"; +import authRoutes from "./routes/auth.routes.js"; +import blogRoutes from "./routes/blog.routes.js"; +import uploadRoutes from "./routes/upload.routes.js"; + +dotenv.config(); + +const app = express(); + +const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS + ? process.env.CORS_ALLOWED_ORIGINS.split(" ") + : ["http://localhost:3001"]; + +const corsOptions = { + origin: function (origin, callback) { + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error("Not allowed by CORS")); + } + }, + methods: ["GET", "POST", "PUT", "DELETE", "PATCH"], + allowedHeaders: "*", +}; + +app.use(express.json()); +app.use(cors(corsOptions)); + +app.use("/api/departments", departmentRoutes); +app.use("/api/auth", authRoutes); +app.use("/api/blogs", blogRoutes); +app.use("/uploads", express.static("uploads")); +app.use("/api/upload", uploadRoutes); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); diff --git a/backend/src/controllers/auth.controller.js b/backend/src/controllers/auth.controller.js new file mode 100644 index 0000000..f679a04 --- /dev/null +++ b/backend/src/controllers/auth.controller.js @@ -0,0 +1,76 @@ +import prisma from "../prisma/client.js"; +import {generateToken} from "../utils/jwt.js"; +import {hashPassword, comparePassword} from "../utils/password.js"; + +/** + * REGISTER + * POST /api/auth/register + */ +export async function register(req, res) { + const {username, password, role} = req.body; + + if (!username || !password) { + return res.status(400).json({error: "Username and password required"}); + } + + const existingUser = await prisma.user.findUnique({ + where: {username}, + }); + + if (existingUser) { + return res.status(409).json({error: "Username already exists"}); + } + + const hashedPassword = await hashPassword(password); + + const user = await prisma.user.create({ + data: { + username, + password: hashedPassword, + role: role || "admin", + }, + }); + + res.status(201).json({ + message: "User registered successfully", + user: { + id: user.id, + username: user.username, + role: user.role, + }, + }); +} + +/** + * LOGIN + * POST /api/auth/login + */ +export async function login(req, res) { + const {username, password} = req.body; + + if (!username || !password) { + return res.status(400).json({error: "Username and password required"}); + } + + const user = await prisma.user.findUnique({ + where: {username}, + }); + + if (!user) { + return res.status(401).json({error: "Invalid credentials"}); + } + + const isValid = await comparePassword(password, user.password); + + if (!isValid) { + return res.status(401).json({error: "Invalid credentials"}); + } + + const token = generateToken({ + userId: user.id, + username: user.username, + role: user.role, + }); + + res.json({token}); +} diff --git a/backend/src/controllers/blog.controller.js b/backend/src/controllers/blog.controller.js new file mode 100644 index 0000000..bf58a09 --- /dev/null +++ b/backend/src/controllers/blog.controller.js @@ -0,0 +1,110 @@ +import prisma from "../prisma/client.js"; + +/* CREATE BLOG */ + +export async function createBlog(req, res) { + const {title, writer, image, content, isActive} = req.body; + + try { + const blog = await prisma.blog.create({ + data: { + title, + writer, + image, + content, + isActive, + }, + }); + + res.json(blog); + } catch (error) { + res.status(500).json({error: "Blog creation failed"}); + } +} + +/* GET ALL BLOGS (Public) */ + +export async function getBlogs(req, res) { + try { + const blogs = await prisma.blog.findMany({ + where: {isActive: true}, + orderBy: {createdAt: "desc"}, + }); + + res.json(blogs); + } catch (error) { + res.status(500).json({error: error.message}); + } +} + +/* GET ALL BLOGS (Admin) */ + +export async function getAllBlogs(req, res) { + try { + const blogs = await prisma.blog.findMany({ + orderBy: {createdAt: "desc"}, + }); + + res.json(blogs); + } catch (error) { + res.status(500).json({error: error.message}); + } +} + +/* GET SINGLE BLOG */ + +export async function getBlog(req, res) { + try { + const id = Number(req.params.id); + + const blog = await prisma.blog.findUnique({ + where: {id}, + }); + + if (!blog) { + return res.status(404).json({error: "Blog not found"}); + } + + res.json(blog); + } catch (error) { + res.status(500).json({error: error.message}); + } +} + +/* UPDATE BLOG */ + +export async function updateBlog(req, res) { + try { + const {title, writer, image, content} = req.body; + + const blog = await prisma.blog.update({ + where: {id: Number(req.params.id)}, + data: { + title, + writer, + image, + content, + }, + }); + + res.json(blog); + } catch (error) { + res.status(500).json({error: error.message}); + } +} + +/* DELETE BLOG */ + +export async function deleteBlog(req, res) { + try { + const id = Number(req.params.id); + + await prisma.blog.delete({ + where: {id}, + }); + + res.json({message: "Blog deleted successfully"}); + } catch (error) { + res.status(500).json({error: error.message}); + } +} diff --git a/backend/src/controllers/department.controller.js b/backend/src/controllers/department.controller.js new file mode 100644 index 0000000..f06f916 --- /dev/null +++ b/backend/src/controllers/department.controller.js @@ -0,0 +1,66 @@ +import prisma from "../prisma/client.js"; + +export const getAllDepartments = async (req, res) => { + try { + const departments = await prisma.department.findMany({ + orderBy: {name: "asc"}, + }); + + const response = departments.map((dep) => ({ + departmentId: dep.departmentId, + Department: dep.name, + para1: dep.para1 ?? "", + para2: dep.para2 ?? "", + para3: dep.para3 ?? "", + facilities: dep.facilities ?? "", + services: dep.services ?? "", + })); + + return res.status(200).json({ + success: true, + data: response, + }); + } catch (error) { + console.error(error); + return res.status(500).json({ + success: false, + message: "Failed to fetch departments", + }); + } +}; + +export async function createDepartment(req, res) { + try { + const {departmentId, name, para1, para2, para3, facilities, services} = + req.body; + + if (!departmentId || !name) { + return res + .status(400) + .json({error: "departmentId and name are required"}); + } + + const department = await prisma.department.create({ + data: { + departmentId, + name, + para1, + para2, + para3, + facilities, + services, + }, + }); + + res.status(201).json({ + message: "Department created successfully", + data: department, + }); + } catch (error) { + if (error.code === "P2002") { + return res.status(409).json({error: "Department already exists"}); + } + + res.status(500).json({error: "Failed to create department"}); + } +} diff --git a/backend/src/controllers/upload.controller.js b/backend/src/controllers/upload.controller.js new file mode 100644 index 0000000..5047ad0 --- /dev/null +++ b/backend/src/controllers/upload.controller.js @@ -0,0 +1,15 @@ +import multer from "multer"; +import path from "path"; + +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, "uploads/blog"); + }, + + filename: function (req, file, cb) { + const fileName = Date.now() + path.extname(file.originalname); + cb(null, fileName); + }, +}); + +export const upload = multer({storage}); diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js new file mode 100644 index 0000000..cf85ca3 --- /dev/null +++ b/backend/src/middleware/auth.js @@ -0,0 +1,19 @@ +import {verifyToken} from "../utils/jwt.js"; + +export default function jwtAuthMiddleware(req, res, next) { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({error: "No token provided"}); + } + + const token = authHeader.split(" ")[1]; + + try { + const user = verifyToken(token); + req.user = user; + next(); + } catch (err) { + return res.status(401).json({error: "Invalid or expired token"}); + } +} diff --git a/backend/src/prisma/client.js b/backend/src/prisma/client.js new file mode 100644 index 0000000..db69ad3 --- /dev/null +++ b/backend/src/prisma/client.js @@ -0,0 +1,5 @@ +import {PrismaClient} from "@prisma/client"; + +const prisma = new PrismaClient(); + +export default prisma; diff --git a/backend/src/routes/auth.routes.js b/backend/src/routes/auth.routes.js new file mode 100644 index 0000000..a121aeb --- /dev/null +++ b/backend/src/routes/auth.routes.js @@ -0,0 +1,9 @@ +import express from "express"; +import {register, login} from "../controllers/auth.controller.js"; + +const router = express.Router(); + +router.post("/register", register); +router.post("/login", login); + +export default router; diff --git a/backend/src/routes/blog.routes.js b/backend/src/routes/blog.routes.js new file mode 100644 index 0000000..7cf9ad2 --- /dev/null +++ b/backend/src/routes/blog.routes.js @@ -0,0 +1,27 @@ +import express from "express"; +import { + createBlog, + getBlogs, + getBlog, + updateBlog, + deleteBlog, + getAllBlogs, +} from "../controllers/blog.controller.js"; + +import jwtAuthMiddleware from "../middleware/auth.js"; + +const router = express.Router(); + +/* PUBLIC */ + +router.get("/", getBlogs); +router.get("/:id", getBlog); + +// Protected + +router.get("/admin/all", jwtAuthMiddleware, getAllBlogs); +router.post("/", jwtAuthMiddleware, createBlog); +router.put("/:id", jwtAuthMiddleware, updateBlog); +router.delete("/:id", jwtAuthMiddleware, deleteBlog); + +export default router; diff --git a/backend/src/routes/department.routes.js b/backend/src/routes/department.routes.js new file mode 100644 index 0000000..ae84d2d --- /dev/null +++ b/backend/src/routes/department.routes.js @@ -0,0 +1,16 @@ +import express from "express"; +import { + getAllDepartments, + createDepartment, +} from "../controllers/department.controller.js"; +import jwtAuthMiddleware from "../middleware/auth.js"; + +const router = express.Router(); + +// Public +router.get("/getAll", getAllDepartments); + +// Protected +router.post("/", jwtAuthMiddleware, createDepartment); + +export default router; diff --git a/backend/src/routes/upload.routes.js b/backend/src/routes/upload.routes.js new file mode 100644 index 0000000..c6a4556 --- /dev/null +++ b/backend/src/routes/upload.routes.js @@ -0,0 +1,15 @@ +import express from "express"; +import {upload} from "../controllers/upload.controller.js"; + +const router = express.Router(); + +router.post("/image", upload.single("image"), (req, res) => { + res.json({ + success: 1, + file: { + url: `http://localhost:3000/uploads/blog/${req.file.filename}`, + }, + }); +}); + +export default router; diff --git a/backend/src/utils/jwt.js b/backend/src/utils/jwt.js new file mode 100644 index 0000000..8678c4c --- /dev/null +++ b/backend/src/utils/jwt.js @@ -0,0 +1,12 @@ +import jwt from "jsonwebtoken"; +import "dotenv/config"; + +const SECRET = process.env.JWT_SECRET; + +export function generateToken(payload) { + return jwt.sign(payload, SECRET, {expiresIn: "24h"}); +} + +export function verifyToken(token) { + return jwt.verify(token, SECRET); +} diff --git a/backend/src/utils/password.js b/backend/src/utils/password.js new file mode 100644 index 0000000..82f9a56 --- /dev/null +++ b/backend/src/utils/password.js @@ -0,0 +1,9 @@ +import bcrypt from "bcryptjs"; + +export async function hashPassword(password) { + return bcrypt.hash(password, 10); +} + +export async function comparePassword(password, hash) { + return bcrypt.compare(password, hash); +} diff --git a/backend/uploads/blog/1773296409367.png b/backend/uploads/blog/1773296409367.png new file mode 100644 index 0000000..2e53760 Binary files /dev/null and b/backend/uploads/blog/1773296409367.png differ diff --git a/backend/uploads/blog/1773298604982.png b/backend/uploads/blog/1773298604982.png new file mode 100644 index 0000000..7ac08ce Binary files /dev/null and b/backend/uploads/blog/1773298604982.png differ diff --git a/backend/uploads/blog/1773298612512.png b/backend/uploads/blog/1773298612512.png new file mode 100644 index 0000000..2e53760 Binary files /dev/null and b/backend/uploads/blog/1773298612512.png differ