feat: basic api setup and boilerplate

This commit is contained in:
ARJUN S THAMPI
2026-03-12 14:15:44 +05:30
commit 521a1fea79
24 changed files with 684 additions and 0 deletions

5
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules
# Keep environment variables out of version control
.env
/src/generated/prisma

35
backend/package.json Normal file
View File

@@ -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"
}
}

14
backend/prisma.config.js Normal file
View File

@@ -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"],
},
});

View File

@@ -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;

View File

@@ -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");

View File

@@ -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")
);

View File

@@ -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"

View File

@@ -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
}

42
backend/src/app.js Normal file
View File

@@ -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}`);
});

View File

@@ -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});
}

View File

@@ -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});
}
}

View File

@@ -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"});
}
}

View File

@@ -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});

View File

@@ -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"});
}
}

View File

@@ -0,0 +1,5 @@
import {PrismaClient} from "@prisma/client";
const prisma = new PrismaClient();
export default prisma;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

12
backend/src/utils/jwt.js Normal file
View File

@@ -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);
}

View File

@@ -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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB