From cb19b5523c3e5613a6a46f0e92c0063096d45c6a Mon Sep 17 00:00:00 2001 From: OthmanImam Date: Thu, 30 Jan 2025 02:02:07 -0800 Subject: [PATCH 1/9] Add Course and UserCourse entities, integrate relationships, update auth middleware for CRUD operations, implement course routes, controller, and service --- .env.example | 10 --- app.js | 29 +++++++++ config/config.js | 38 ++++++++---- controllers/course.controller.js | 97 +++++++++++++++++++++++++++++ entities/course.entity.js | 61 +++++++++++++++++++ entities/job.entity.js | 3 +- entities/user.entity.js | 61 +++++++++++-------- entities/userCourse.entity.js | 45 ++++++++++++++ http/courseRoute.http | 46 ++++++++++++++ middlewares/auth.middleware.js | 19 ++++++ package-lock.json | 6 +- routes/courseRoutes.js | 38 ++++++++++++ server.js | 60 +++++++++--------- services/courseServices.js | 101 +++++++++++++++++++++++++++++++ services/jobServices.js | 2 +- validation/course.validation.js | 31 ++++++++++ 16 files changed, 570 insertions(+), 77 deletions(-) delete mode 100644 .env.example create mode 100644 entities/course.entity.js create mode 100644 entities/userCourse.entity.js create mode 100644 http/courseRoute.http create mode 100644 validation/course.validation.js diff --git a/.env.example b/.env.example deleted file mode 100644 index f04b419..0000000 --- a/.env.example +++ /dev/null @@ -1,10 +0,0 @@ -DB_HOST=localhost -DB_PORT=5432 -DB_USER=postgres -DB_PASSWORD=your_password -DB_NAME=skillnet -JWT_SECRET=your_jwt_secret_key -PORT=5000 -CLOUDINARY_CLOUD_NAME=your_cloud_name -CLOUDINARY_API_KEY=your_api_key -CLOUDINARY_API_SECRET=your_api_secret \ No newline at end of file diff --git a/app.js b/app.js index e69de29..0355da3 100644 --- a/app.js +++ b/app.js @@ -0,0 +1,29 @@ +import dotenv from "dotenv" +import express from "express" +import AppDataSource from "./config/config.js" + +dotenv.config() + +const app = express() +const PORT = process.env.PORT || 3000 + +app.use(express.json()) + +async function startServer() { + try { + await AppDataSource.initialize() + console.log("Data Source has been initialized!") + + app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`) + }) + } catch (err) { + console.error("Error during Data Source initialization", err) + process.exit(1) + } +} + +startServer() + +export default app + diff --git a/config/config.js b/config/config.js index fe17fd4..0b99df5 100644 --- a/config/config.js +++ b/config/config.js @@ -1,20 +1,36 @@ -import { DataSource } from 'typeorm'; -import User from '../entities/user.entity.js'; -import Job from '../entities/job.entity.js'; +import dotenv from "dotenv" +import { DataSource } from "typeorm" +import { User } from "../entities/user.entity.js" +import { Course } from "../entities/course.entity.js" +import { UserCourse } from "../entities/userCourse.entity.js" +import {Job} from "../entities/job.entity.js" + +dotenv.config() + +function logDatabaseConfig() { + console.log("Database Configuration:") + console.log(`Host: ${process.env.DB_HOST || "localhost"}`) + console.log(`Port: ${process.env.DB_PORT || "5432"}`) + console.log(`Username: ${process.env.DB_USER}`) + console.log(`Database: ${process.env.DB_NAME}`) + console.log(`Password is set: ${Boolean(process.env.DB_PASSWORD)}`) +} + +logDatabaseConfig() const AppDataSource = new DataSource({ - type: 'postgres', - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '5432'), + type: "postgres", + host: process.env.DB_HOST, + port: Number.parseInt(process.env.DB_PORT || "5432"), username: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, synchronize: true, - dropSchema: false, - logging: false, - entities: [User, Job], + logging: true, + entities: [User, Course, UserCourse, Job], subscribers: [], migrations: [], -}); +}) + +export default AppDataSource -export default AppDataSource; \ No newline at end of file diff --git a/controllers/course.controller.js b/controllers/course.controller.js index e69de29..b227302 100644 --- a/controllers/course.controller.js +++ b/controllers/course.controller.js @@ -0,0 +1,97 @@ +import { CourseService } from "../services/courseServices.js" +import courseValidation from "../validation/course.validation.js" + +class CourseController { + async createCourse(req, res) { + try { + const { error, value } = courseValidation.createCourse.validate(req.body) + if (error) { + return res.status(400).json({ message: error.details[0].message }) + } + + const course = await CourseService.createCourse(value) + res.status(201).json(course) + } catch (error) { + if (error.message === "Course already exists") { + res.status(409).json({ message: error.message }) + } else { + res.status(400).json({ message: "Error creating course", error: error.message }) + } + } + } + + async getAllCourses(req, res) { + try { + const courses = await CourseService.getAllCourses() + res.json(courses) + } catch (error) { + res.status(500).json({ message: "Error fetching courses", error: error.message }) + } + } + + async getCourseById(req, res) { + try { + const { error, value } = courseValidation.courseIdParam.validate({ id: req.params.id }) + if (error) { + return res.status(400).json({ message: error.details[0].message }) + } + + const course = await CourseService.getCourseById(value.id) + if (course) { + res.json(course) + } else { + res.status(404).json({ message: "Course not found" }) + } + } catch (error) { + res.status(500).json({ message: "Error fetching course", error: error.message }) + } + } + + async updateCourse(req, res) { + try { + const idValidation = courseValidation.courseIdParam.validate({ id: req.params.id }) + if (idValidation.error) { + return res.status(400).json({ message: idValidation.error.details[0].message }) + } + + const { error, value } = courseValidation.updateCourse.validate(req.body) + if (error) { + return res.status(400).json({ message: error.details[0].message }) + } + + const course = await CourseService.updateCourse(idValidation.value.id, value) + if (course) { + res.json(course) + } else { + res.status(404).json({ message: "Course not found" }) + } + } catch (error) { + if (error.message === "Course already exists") { + res.status(409).json({ message: error.message }) + } else { + res.status(400).json({ message: "Error updating course", error: error.message }) + } + } + } + + async deleteCourse(req, res) { + try { + const { error, value } = courseValidation.courseIdParam.validate({ id: req.params.id }) + if (error) { + return res.status(400).json({ message: error.details[0].message }) + } + + await CourseService.deleteCourse(value.id) + res.status(204).send() + } catch (error) { + if (error.message === "Course not found") { + res.status(404).json({ message: error.message }) + } else { + res.status(500).json({ message: "Error deleting course", error: error.message }) + } + } + } +} + +export default new CourseController() + diff --git a/entities/course.entity.js b/entities/course.entity.js new file mode 100644 index 0000000..1cb11e6 --- /dev/null +++ b/entities/course.entity.js @@ -0,0 +1,61 @@ +import { EntitySchema } from "typeorm" + +export const Course = new EntitySchema({ + name: "Course", + tableName: "courses", + columns: { + id: { + type: "int", + primary: true, + generated: true, + }, + title: { + type: "varchar", + unique: true, + nullable: false, + }, + description: { + type: "text", + nullable: true, + }, + instructorId: { + type: "int", + nullable: false, + }, + level: { + type: "varchar", + nullable: true, + }, + duration: { + type: "int", + nullable: true, + }, + createdAt: { + type: "timestamp", + createDate: true, + }, + }, + relations: { + users: { + type: "many-to-many", + target: "User", + joinTable: { + name: "user_courses", + joinColumn: { + name: "courseId", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "userId", + referencedColumnName: "id", + }, + }, + }, + userCourses: { + type: "one-to-many", + target: "UserCourse", + inverseSide: "course", + }, + }, +}) + diff --git a/entities/job.entity.js b/entities/job.entity.js index e154889..c8effee 100644 --- a/entities/job.entity.js +++ b/entities/job.entity.js @@ -1,6 +1,6 @@ import { EntitySchema } from 'typeorm'; -const Job = new EntitySchema({ +export const Job = new EntitySchema({ name: 'Job', tableName: 'jobs', columns: { @@ -59,4 +59,3 @@ const Job = new EntitySchema({ ] }); -export default Job; \ No newline at end of file diff --git a/entities/user.entity.js b/entities/user.entity.js index 2b63863..ffa1af8 100644 --- a/entities/user.entity.js +++ b/entities/user.entity.js @@ -1,57 +1,70 @@ -import { EntitySchema } from 'typeorm'; +import { EntitySchema } from "typeorm" -const UserEntity = new EntitySchema({ - name: 'User', - tableName: 'users', +export const User = new EntitySchema({ + name: "User", + tableName: "users", columns: { id: { primary: true, - type: 'int', + type: "int", generated: true, }, walletAddress: { - type: 'varchar', + type: "varchar", unique: true, }, role: { - type: 'varchar', + type: "varchar", length: 20, }, - // Generalizes to job seeker/employer/institution name name: { - type: 'varchar', + type: "varchar", length: 100, }, email: { - type: 'varchar', + type: "varchar", unique: true, }, - // Tutor-specific (nullable) title: { - type: 'varchar', + type: "varchar", length: 100, nullable: true, }, - // Used as bio (Job Seeker, Tutor, Institution)/description (Company) bio: { - type: 'text', + type: "text", nullable: true, }, - // Tutor-specific (nullable) skills: { - type: 'simple-array', + type: "simple-array", nullable: true, }, createdAt: { - type: 'timestamp', + type: "timestamp", createDate: true, }, }, - indices: [ - { columns: ['walletAddress'] }, - { columns: ['email'] }, - { columns: ['role'] }, // Index role if querying by it frequently - ], -}); + relations: { + courses: { + type: "many-to-many", + target: "Course", + joinTable: { + name: "user_courses", + joinColumn: { + name: "userId", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "courseId", + referencedColumnName: "id", + }, + }, + }, + userCourses: { + type: "one-to-many", + target: "UserCourse", + inverseSide: "user", + }, + }, + indices: [{ columns: ["walletAddress"] }, { columns: ["email"] }, { columns: ["role"] }], +}) -export default UserEntity; \ No newline at end of file diff --git a/entities/userCourse.entity.js b/entities/userCourse.entity.js new file mode 100644 index 0000000..4ff8334 --- /dev/null +++ b/entities/userCourse.entity.js @@ -0,0 +1,45 @@ +import { EntitySchema } from "typeorm" + +export const UserCourse = new EntitySchema({ + name: "UserCourse", + tableName: "user_courses", + columns: { + userId: { + primary: true, + type: "int", + }, + courseId: { + primary: true, + type: "int", + }, + progress: { + type: "int", + default: 0, + }, + enrollmentDate: { + type: "timestamp", + createDate: true, + }, + }, + relations: { + user: { + type: "many-to-one", + target: "User", + joinColumn: { + name: "userId", + referencedColumnName: "id", + }, + onDelete: "CASCADE", + }, + course: { + type: "many-to-one", + target: "Course", + joinColumn: { + name: "courseId", + referencedColumnName: "id", + }, + onDelete: "CASCADE", + }, + }, +}) + diff --git a/http/courseRoute.http b/http/courseRoute.http new file mode 100644 index 0000000..6e0c0e0 --- /dev/null +++ b/http/courseRoute.http @@ -0,0 +1,46 @@ +@baseUrl = http://localhost:3000/api/courses + +### Create a new course +POST {{baseUrl}} +Content-Type: application/json + +{ + "title": "Introduction to Blockchain", + "description": "Learn the basics of blockchain technology", + "instructorId": 1, + "level": "beginner", + "duration": 5 +} + +### Get all courses +GET {{baseUrl}} + +### Get a specific course +GET {{baseUrl}}/1 + +### Update a course +PUT {{baseUrl}}/1 +Content-Type: application/json +{ + "title": "Advanced Blockchain Concepts", + "description": "Dive deeper into blockchain technology and its applications" +} + +### Delete a course +DELETE {{baseUrl}}/1 + +### Create another course +POST {{baseUrl}} +Content-Type: application/json + +{ + "title": "Advance Smart Contract Development", + "description": "Learn how to create and deploy smart contracts on blockchain platforms", + "instructorId": 2, + "level": "beginner", + "duration": 5 +} + +### Get all courses after operations +GET {{baseUrl}} + diff --git a/middlewares/auth.middleware.js b/middlewares/auth.middleware.js index e69de29..00440c1 100644 --- a/middlewares/auth.middleware.js +++ b/middlewares/auth.middleware.js @@ -0,0 +1,19 @@ +import jwt from "jsonwebtoken" + +const authenticateToken = (req, res, next) => { + // For now, we'll set a mock user object for all requests + req.user = { + id: 1, + username: "mockuser", + role: "instructor", + } + next() +} + +const isInstructor = (req, res, next) => { + // For now, we'll assume all users are instructors + next() +} + +export { authenticateToken, isInstructor } + diff --git a/package-lock.json b/package-lock.json index 5739349..09307e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2102,7 +2102,8 @@ "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.3.0", @@ -2798,6 +2799,7 @@ "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -3258,6 +3260,7 @@ "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -4840,6 +4843,7 @@ "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", diff --git a/routes/courseRoutes.js b/routes/courseRoutes.js index e69de29..bcf0257 100644 --- a/routes/courseRoutes.js +++ b/routes/courseRoutes.js @@ -0,0 +1,38 @@ +import express from "express" +import multer from "multer" +import courseController from "../controllers/course.controller.js" +import { authenticateToken, isInstructor } from "../middlewares/auth.middleware.js" + +const router = express.Router() + +// Configure multer for file upload +const storage = multer.diskStorage({ + destination: "./uploads/courses", + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9) + cb(null, file.fieldname + "-" + uniqueSuffix + "." + file.originalname.split(".").pop()) + }, +}) + +const upload = multer({ + storage: storage, + limits: { + fileSize: 10 * 1024 * 1024, // 10MB limit + }, + fileFilter: (req, file, cb) => { + if (file.mimetype.startsWith("image/") || file.mimetype === "application/pdf") { + cb(null, true) + } else { + cb(new Error("Only image and PDF files are allowed!"), false) + } + }, +}) + +router.post("/", authenticateToken, isInstructor, upload.single("courseMaterial"), courseController.createCourse) +router.get("/", courseController.getAllCourses) +router.get("/:id", courseController.getCourseById) +router.put("/:id", authenticateToken, isInstructor, upload.single("courseMaterial"), courseController.updateCourse) +router.delete("/:id", authenticateToken, isInstructor, courseController.deleteCourse) + +export default router + diff --git a/server.js b/server.js index 1f0fb6e..73d2f85 100644 --- a/server.js +++ b/server.js @@ -1,40 +1,44 @@ import 'dotenv/config' -import AppDataSource from "./config/config.js"; +import AppDataSource from "./config/config.js" import express, { json } from 'express'; import authRouter from './routes/auth.routes.js'; import jobRoutes from './routes/jobRoutes.js'; +import courseRoutes from "./routes/courseRoutes.js" import helmet from 'helmet'; import cors from 'cors'; -const PORT = process.env.PORT || 5000; +// Load environment variables +const PORT = process.env.PORT || 3000 +// Initialize database connection and start server AppDataSource.initialize() - .then(() => { - console.log('Database connected'); - - const app = express(); - - // Middleware - app.use(helmet()); - app.use(cors()); - app.use(json()); - - // Routes - app.use('/api/auth', authRouter); - app.use('/api/jobs', jobRoutes); - - // Error handling - app.use((err, req, res, next) => { - console.error(err.stack); - res.status(500).json({ error: 'Internal server error' }); - }); - +.then(async () => { + console.log("Data Source has been initialized!") + + const app = express() + + // Middleware + app.use(helmet()); + app.use(cors()); + app.use(json()); + + // Routes + app.use('/api/auth', authRouter); + app.use('/api/jobs', jobRoutes); + app.use("/api/courses", courseRoutes) + + // Synchronize the entities with the database + await AppDataSource.synchronize() + console.log("Database schema has been synchronized") + app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); - }); + console.log(`Server is running on port ${PORT}`) + }) + }) - .catch((error) => { - console.error('Database connection failed:', error); - process.exit(1); - }); \ No newline at end of file + .catch((err) => { + console.error('Database connection failed:', error); + process.exit(1); + }) + diff --git a/services/courseServices.js b/services/courseServices.js index e69de29..19b0c1f 100644 --- a/services/courseServices.js +++ b/services/courseServices.js @@ -0,0 +1,101 @@ +import AppDataSource from "../config/config.js" +import { Course } from "../entities/course.entity.js" + +const courseRepository = AppDataSource.getRepository(Course) + +class CourseError extends Error { + constructor(message, statusCode = 500) { + super(message) + this.name = "CourseError" + this.statusCode = statusCode + } +} + +export const CourseService = { + async createCourse(courseData) { + try { + // Check if a course with the same title already exists + const existingCourse = await courseRepository.findOne({ where: { title: courseData.title } }) + if (existingCourse) { + throw new CourseError("A course with this title already exists", 409) // 409 Conflict + } + + const course = courseRepository.create(courseData) + return await courseRepository.save(course) + } catch (error) { + if (error instanceof CourseError) { + throw error + } + throw new CourseError("Failed to create course", 400) + } + }, + + async getAllCourses() { + try { + return await courseRepository.find() + } catch (error) { + throw new CourseError("Failed to fetch courses") + } + }, + + async getCourseById(id) { + try { + const course = await courseRepository.findOneBy({ id }) + if (!course) { + throw new CourseError("Course not found", 404) + } + return course + } catch (error) { + if (error instanceof CourseError) { + throw error + } + throw new CourseError("Failed to fetch course") + } + }, + + async updateCourse(id, courseData) { + try { + // Check if the updated title already exists for another course + if (courseData.title) { + const existingCourse = await courseRepository.findOne({ + where: { title: courseData.title, id: { $ne: id } }, + }) + if (existingCourse) { + throw new CourseError("A course with this title already exists", 409) + } + } + + const updateResult = await courseRepository.update(id, courseData) + if (updateResult.affected === 0) { + throw new CourseError("Course not found", 404) + } + const updatedCourse = await courseRepository.findOneBy({ id }) + if (!updatedCourse) { + throw new CourseError("Failed to fetch updated course") + } + return updatedCourse + } catch (error) { + if (error instanceof CourseError) { + throw error + } + throw new CourseError("Failed to update course") + } + }, + + async deleteCourse(id) { + try { + const deleteResult = await courseRepository.delete(id) + if (deleteResult.affected === 0) { + throw new CourseError("Course not found", 404) + } + } catch (error) { + if (error instanceof CourseError) { + throw error + } + throw new CourseError("Failed to delete course") + } + }, +} + +// export default courseService + diff --git a/services/jobServices.js b/services/jobServices.js index f32d0e4..2ca52fa 100644 --- a/services/jobServices.js +++ b/services/jobServices.js @@ -1,5 +1,5 @@ import AppDataSource from "../config/config.js"; -import Job from "../entities/job.entity.js"; +import {Job} from "../entities/job.entity.js"; import JobValidation from "../validation/job.validation.js"; import UploadService from "./upload.service.js"; diff --git a/validation/course.validation.js b/validation/course.validation.js new file mode 100644 index 0000000..4504570 --- /dev/null +++ b/validation/course.validation.js @@ -0,0 +1,31 @@ +import Joi from "joi" + +const courseValidation = { + createCourse: Joi.object({ + title: Joi.string().required().min(3).max(100), + description: Joi.string().required().min(10).max(1000), + instructorId: Joi.number().integer().positive().required(), + duration: Joi.number().integer().positive().required(), + level: Joi.string().valid("beginner", "intermediate", "advanced").required(), + tags: Joi.array().items(Joi.string()).min(1).max(5), + price: Joi.number().precision(2).min(0), + isPublished: Joi.boolean().default(false), + }), + + updateCourse: Joi.object({ + title: Joi.string().min(3).max(100), + description: Joi.string().min(10).max(1000), + duration: Joi.number().integer().positive(), + level: Joi.string().valid("beginner", "intermediate", "advanced"), + tags: Joi.array().items(Joi.string()).min(1).max(5), + price: Joi.number().precision(2).min(0), + isPublished: Joi.boolean(), + }).min(1), // At least one field must be present for update + + courseIdParam: Joi.object({ + id: Joi.number().integer().positive().required(), + }), +} + +export default courseValidation + From 27a032399fb9dd37120bcc27e750fed57648a210 Mon Sep 17 00:00:00 2001 From: OthmanImam Date: Fri, 31 Jan 2025 00:22:44 -0800 Subject: [PATCH 2/9] updated the config file --- config/config.js | 11 ----------- http/courseRoute.http | 4 ++-- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/config/config.js b/config/config.js index 0b99df5..03359e4 100644 --- a/config/config.js +++ b/config/config.js @@ -7,17 +7,6 @@ import {Job} from "../entities/job.entity.js" dotenv.config() -function logDatabaseConfig() { - console.log("Database Configuration:") - console.log(`Host: ${process.env.DB_HOST || "localhost"}`) - console.log(`Port: ${process.env.DB_PORT || "5432"}`) - console.log(`Username: ${process.env.DB_USER}`) - console.log(`Database: ${process.env.DB_NAME}`) - console.log(`Password is set: ${Boolean(process.env.DB_PASSWORD)}`) -} - -logDatabaseConfig() - const AppDataSource = new DataSource({ type: "postgres", host: process.env.DB_HOST, diff --git a/http/courseRoute.http b/http/courseRoute.http index 6e0c0e0..02ae044 100644 --- a/http/courseRoute.http +++ b/http/courseRoute.http @@ -5,7 +5,7 @@ POST {{baseUrl}} Content-Type: application/json { - "title": "Introduction to Blockchain", + "title": "Advance Blockchain", "description": "Learn the basics of blockchain technology", "instructorId": 1, "level": "beginner", @@ -27,7 +27,7 @@ Content-Type: application/json } ### Delete a course -DELETE {{baseUrl}}/1 +DELETE {{baseUrl}}/4 ### Create another course POST {{baseUrl}} From 6c863d1f7c1041f670d665e939f3687fe1db510b Mon Sep 17 00:00:00 2001 From: OthmanImam Date: Fri, 31 Jan 2025 08:05:04 -0800 Subject: [PATCH 3/9] Updated the couseEntity to have Course video record stored --- controllers/course.controller.js | 29 ++++++++++++++ entities/course.entity.js | 4 ++ routes/courseRoutes.js | 65 ++++++++++++++++++-------------- services/courseServices.js | 21 ++++++++++- validation/course.validation.js | 6 +++ 5 files changed, 95 insertions(+), 30 deletions(-) diff --git a/controllers/course.controller.js b/controllers/course.controller.js index b227302..7a99488 100644 --- a/controllers/course.controller.js +++ b/controllers/course.controller.js @@ -91,6 +91,35 @@ class CourseController { } } } + + async updateCourseVideo(req, res) { + try { + const { error: idError, value: idValue } = courseValidation.courseIdParam.validate({ id: req.params.id }) + if (idError) { + return res.status(400).json({ message: idError.details[0].message }) + } + + if (!req.file) { + return res.status(400).json({ message: "No video file uploaded" }) + } + + const videoUrl = `/uploads/courses/videos/${req.file.filename}` + + const { error, value } = courseValidation.updateCourseVideo.validate({ videoUrl }) + if (error) { + return res.status(400).json({ message: error.details[0].message }) + } + + const updatedCourse = await CourseService.updateCourseVideo(idValue.id, value.videoUrl) + if (updatedCourse) { + res.json(updatedCourse) + } else { + res.status(404).json({ message: "Course not found" }) + } + } catch (error) { + res.status(500).json({ message: "Error updating course video", error: error.message }) + } + } } export default new CourseController() diff --git a/entities/course.entity.js b/entities/course.entity.js index 1cb11e6..09c48f8 100644 --- a/entities/course.entity.js +++ b/entities/course.entity.js @@ -30,6 +30,10 @@ export const Course = new EntitySchema({ type: "int", nullable: true, }, + videoUrl: { + type: "varchar", + nullable: true, + }, createdAt: { type: "timestamp", createDate: true, diff --git a/routes/courseRoutes.js b/routes/courseRoutes.js index bcf0257..2f8446e 100644 --- a/routes/courseRoutes.js +++ b/routes/courseRoutes.js @@ -1,38 +1,45 @@ import express from "express" import multer from "multer" -import courseController from "../controllers/course.controller.js" import { authenticateToken, isInstructor } from "../middlewares/auth.middleware.js" +import CourseController from "../controllers/course.controller.js" const router = express.Router() +const upload = multer() -// Configure multer for file upload -const storage = multer.diskStorage({ - destination: "./uploads/courses", - filename: (req, file, cb) => { - const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9) - cb(null, file.fieldname + "-" + uniqueSuffix + "." + file.originalname.split(".").pop()) - }, -}) - -const upload = multer({ - storage: storage, - limits: { - fileSize: 10 * 1024 * 1024, // 10MB limit - }, - fileFilter: (req, file, cb) => { - if (file.mimetype.startsWith("image/") || file.mimetype === "application/pdf") { - cb(null, true) - } else { - cb(new Error("Only image and PDF files are allowed!"), false) - } - }, -}) - -router.post("/", authenticateToken, isInstructor, upload.single("courseMaterial"), courseController.createCourse) -router.get("/", courseController.getAllCourses) -router.get("/:id", courseController.getCourseById) -router.put("/:id", authenticateToken, isInstructor, upload.single("courseMaterial"), courseController.updateCourse) -router.delete("/:id", authenticateToken, isInstructor, courseController.deleteCourse) +router.post( + "/", + authenticateToken, + isInstructor, + upload.fields([ + { name: "courseMaterial", maxCount: 1 }, + { name: "courseVideo", maxCount: 1 }, + ]), + CourseController.createCourse, +) + +router.get("/", CourseController.getAllCourses) +router.get("/:id", CourseController.getCourseById) + +router.put( + "/:id", + authenticateToken, + isInstructor, + upload.fields([ + { name: "courseMaterial", maxCount: 1 }, + { name: "courseVideo", maxCount: 1 }, + ]), + CourseController.updateCourse, +) + +router.delete("/:id", authenticateToken, isInstructor, CourseController.deleteCourse) + +router.put( + "/:id/video", + authenticateToken, + isInstructor, + upload.single("courseVideo"), + CourseController.updateCourseVideo, +) export default router diff --git a/services/courseServices.js b/services/courseServices.js index 19b0c1f..0059520 100644 --- a/services/courseServices.js +++ b/services/courseServices.js @@ -95,7 +95,26 @@ export const CourseService = { throw new CourseError("Failed to delete course") } }, + + async updateCourseVideo(id, videoUrl) { + try { + const updateResult = await courseRepository.update(id, { videoUrl }) + if (updateResult.affected === 0) { + throw new CourseError("Course not found", 404) + } + const updatedCourse = await courseRepository.findOneBy({ id }) + if (!updatedCourse) { + throw new CourseError("Failed to fetch updated course") + } + return updatedCourse + } catch (error) { + if (error instanceof CourseError) { + throw error + } + throw new CourseError("Failed to update course video") + } + }, } -// export default courseService +// export default CourseService diff --git a/validation/course.validation.js b/validation/course.validation.js index 4504570..3f6d1ce 100644 --- a/validation/course.validation.js +++ b/validation/course.validation.js @@ -10,6 +10,7 @@ const courseValidation = { tags: Joi.array().items(Joi.string()).min(1).max(5), price: Joi.number().precision(2).min(0), isPublished: Joi.boolean().default(false), + videoUrl: Joi.string().uri().allow(null, ""), // Allow null or empty string for initial creation without video }), updateCourse: Joi.object({ @@ -20,8 +21,13 @@ const courseValidation = { tags: Joi.array().items(Joi.string()).min(1).max(5), price: Joi.number().precision(2).min(0), isPublished: Joi.boolean(), + videoUrl: Joi.string().uri().allow(null, ""), }).min(1), // At least one field must be present for update + updateCourseVideo: Joi.object({ + videoUrl: Joi.string().uri().required(), + }), + courseIdParam: Joi.object({ id: Joi.number().integer().positive().required(), }), From 15cea273e23f15a1bd83a251b86254471e40a08e Mon Sep 17 00:00:00 2001 From: OthmanImam Date: Fri, 31 Jan 2025 08:18:50 -0800 Subject: [PATCH 4/9] resoved the dotenv issue --- .env.example | 10 ++++++++++ server.js | 5 ++--- 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..39063b8 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +DB_HOST=localhost +DB_PORT=5432 +DB_USER=postgres +DB_PASSWORD=BNG482@AA +DB_NAME=skillNet +JWT_SECRET=your_jwt_secret_key +PORT=3000 +CLOUDINARY_CLOUD_NAME=your_cloud_name +CLOUDINARY_API_KEY=your_api_key +CLOUDINARY_API_SECRET=your_api_secret \ No newline at end of file diff --git a/server.js b/server.js index 73d2f85..2e2d654 100644 --- a/server.js +++ b/server.js @@ -38,7 +38,6 @@ AppDataSource.initialize() }) .catch((err) => { - console.error('Database connection failed:', error); + console.error('Database connection failed:', err); process.exit(1); - }) - + }) \ No newline at end of file From d6709e83532803e43238c0e6ab0110f964e4e782 Mon Sep 17 00:00:00 2001 From: OthmanImam Date: Fri, 31 Jan 2025 08:28:43 -0800 Subject: [PATCH 5/9] updated the inport of entities in the connfig file --- .env.example | 6 +++--- config/config.js | 6 +++--- entities/job.entity.js | 3 ++- entities/user.entity.js | 3 ++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 39063b8..f04b419 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,10 @@ DB_HOST=localhost DB_PORT=5432 DB_USER=postgres -DB_PASSWORD=BNG482@AA -DB_NAME=skillNet +DB_PASSWORD=your_password +DB_NAME=skillnet JWT_SECRET=your_jwt_secret_key -PORT=3000 +PORT=5000 CLOUDINARY_CLOUD_NAME=your_cloud_name CLOUDINARY_API_KEY=your_api_key CLOUDINARY_API_SECRET=your_api_secret \ No newline at end of file diff --git a/config/config.js b/config/config.js index 03359e4..0d1fe82 100644 --- a/config/config.js +++ b/config/config.js @@ -1,16 +1,16 @@ import dotenv from "dotenv" import { DataSource } from "typeorm" -import { User } from "../entities/user.entity.js" import { Course } from "../entities/course.entity.js" import { UserCourse } from "../entities/userCourse.entity.js" -import {Job} from "../entities/job.entity.js" +import User from "../entities/user.entity.js" +import Job from "../entities/job.entity.js" dotenv.config() const AppDataSource = new DataSource({ type: "postgres", host: process.env.DB_HOST, - port: Number.parseInt(process.env.DB_PORT || "5432"), + port: Number.parseInt(process.env.DB_PORT), username: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, diff --git a/entities/job.entity.js b/entities/job.entity.js index c8effee..a8ee775 100644 --- a/entities/job.entity.js +++ b/entities/job.entity.js @@ -1,6 +1,6 @@ import { EntitySchema } from 'typeorm'; -export const Job = new EntitySchema({ +const Job = new EntitySchema({ name: 'Job', tableName: 'jobs', columns: { @@ -59,3 +59,4 @@ export const Job = new EntitySchema({ ] }); +export default Job \ No newline at end of file diff --git a/entities/user.entity.js b/entities/user.entity.js index ffa1af8..32faf32 100644 --- a/entities/user.entity.js +++ b/entities/user.entity.js @@ -1,6 +1,6 @@ import { EntitySchema } from "typeorm" -export const User = new EntitySchema({ +const User = new EntitySchema({ name: "User", tableName: "users", columns: { @@ -68,3 +68,4 @@ export const User = new EntitySchema({ indices: [{ columns: ["walletAddress"] }, { columns: ["email"] }, { columns: ["role"] }], }) +export default User \ No newline at end of file From 5f551eb1b361ec3828d87a2269cb4062bf2eaf91 Mon Sep 17 00:00:00 2001 From: OthmanImam Date: Fri, 31 Jan 2025 08:36:34 -0800 Subject: [PATCH 6/9] updated inport in the jobService --- server.js | 6 ++---- services/jobServices.js | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/server.js b/server.js index 2e2d654..f5e60c8 100644 --- a/server.js +++ b/server.js @@ -30,14 +30,12 @@ AppDataSource.initialize() // Synchronize the entities with the database await AppDataSource.synchronize() - console.log("Database schema has been synchronized") - app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`) }) }) - .catch((err) => { - console.error('Database connection failed:', err); + .catch((error) => { + console.error('Database connection failed:', error); process.exit(1); }) \ No newline at end of file diff --git a/services/jobServices.js b/services/jobServices.js index 2ca52fa..f32d0e4 100644 --- a/services/jobServices.js +++ b/services/jobServices.js @@ -1,5 +1,5 @@ import AppDataSource from "../config/config.js"; -import {Job} from "../entities/job.entity.js"; +import Job from "../entities/job.entity.js"; import JobValidation from "../validation/job.validation.js"; import UploadService from "./upload.service.js"; From c643ff814219656201a18a4c4f7a087978dfad8f Mon Sep 17 00:00:00 2001 From: OthmanImam Date: Fri, 31 Jan 2025 08:45:02 -0800 Subject: [PATCH 7/9] updated naming convention conflic resolve --- config/config.js | 6 +++--- entities/course.entity.js | 2 +- entities/job.entity.js | 2 +- entities/user.entity.js | 12 ++++++++---- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/config/config.js b/config/config.js index 0d1fe82..3a40600 100644 --- a/config/config.js +++ b/config/config.js @@ -2,7 +2,7 @@ import dotenv from "dotenv" import { DataSource } from "typeorm" import { Course } from "../entities/course.entity.js" import { UserCourse } from "../entities/userCourse.entity.js" -import User from "../entities/user.entity.js" +import UserEntity from "../entities/user.entity.js" import Job from "../entities/job.entity.js" dotenv.config() @@ -16,10 +16,10 @@ const AppDataSource = new DataSource({ database: process.env.DB_NAME, synchronize: true, logging: true, - entities: [User, Course, UserCourse, Job], + entities: [UserEntity, Course, UserCourse, Job], subscribers: [], migrations: [], }) -export default AppDataSource +export default AppDataSource; diff --git a/entities/course.entity.js b/entities/course.entity.js index 09c48f8..286dd59 100644 --- a/entities/course.entity.js +++ b/entities/course.entity.js @@ -42,7 +42,7 @@ export const Course = new EntitySchema({ relations: { users: { type: "many-to-many", - target: "User", + target: "UserEntity", joinTable: { name: "user_courses", joinColumn: { diff --git a/entities/job.entity.js b/entities/job.entity.js index a8ee775..e154889 100644 --- a/entities/job.entity.js +++ b/entities/job.entity.js @@ -59,4 +59,4 @@ const Job = new EntitySchema({ ] }); -export default Job \ No newline at end of file +export default Job; \ No newline at end of file diff --git a/entities/user.entity.js b/entities/user.entity.js index 32faf32..a89d726 100644 --- a/entities/user.entity.js +++ b/entities/user.entity.js @@ -1,6 +1,6 @@ -import { EntitySchema } from "typeorm" +import { EntitySchema } from "typeorm"; -const User = new EntitySchema({ +const UserEntity = new EntitySchema({ name: "User", tableName: "users", columns: { @@ -65,7 +65,11 @@ const User = new EntitySchema({ inverseSide: "user", }, }, - indices: [{ columns: ["walletAddress"] }, { columns: ["email"] }, { columns: ["role"] }], + indices: [ + { columns: ['walletAddress'] }, + { columns: ['email'] }, + { columns: ['role'] }, // Index role if querying by it frequently + ], }) -export default User \ No newline at end of file +export default UserEntity \ No newline at end of file From 922072c30e0fc3a305601d7795325fe9f26ac32e Mon Sep 17 00:00:00 2001 From: OthmanImam Date: Fri, 31 Jan 2025 12:08:06 -0800 Subject: [PATCH 8/9] adjusted the server file and config --- config/config.js | 6 +++--- entities/user.entity.js | 48 ++++++++++++++++++++--------------------- server.js | 11 +++++++--- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/config/config.js b/config/config.js index 3a40600..83a870c 100644 --- a/config/config.js +++ b/config/config.js @@ -1,9 +1,9 @@ import dotenv from "dotenv" -import { DataSource } from "typeorm" +import { DataSource } from "typeorm"; import { Course } from "../entities/course.entity.js" import { UserCourse } from "../entities/userCourse.entity.js" -import UserEntity from "../entities/user.entity.js" -import Job from "../entities/job.entity.js" +import UserEntity from "../entities/user.entity.js"; +import Job from "../entities/job.entity.js"; dotenv.config() diff --git a/entities/user.entity.js b/entities/user.entity.js index a89d726..1619ac5 100644 --- a/entities/user.entity.js +++ b/entities/user.entity.js @@ -1,68 +1,68 @@ -import { EntitySchema } from "typeorm"; +import { EntitySchema } from 'typeorm'; const UserEntity = new EntitySchema({ - name: "User", - tableName: "users", + name: 'User', + tableName: 'users', columns: { id: { primary: true, - type: "int", + type: 'int', generated: true, }, walletAddress: { - type: "varchar", + type: 'varchar', unique: true, }, role: { - type: "varchar", + type: 'varchar', length: 20, }, name: { - type: "varchar", + type: 'varchar', length: 100, }, email: { - type: "varchar", + type: 'varchar', unique: true, }, title: { - type: "varchar", + type: 'varchar', length: 100, nullable: true, }, bio: { - type: "text", + type: 'text', nullable: true, }, skills: { - type: "simple-array", + type: 'simple-array', nullable: true, }, createdAt: { - type: "timestamp", + type: 'timestamp', createDate: true, }, }, relations: { courses: { - type: "many-to-many", - target: "Course", + type: 'many-to-many', + target: 'Course', joinTable: { - name: "user_courses", + name: 'user_courses', joinColumn: { - name: "userId", - referencedColumnName: "id", + name: 'userId', + referencedColumnName: 'id', }, inverseJoinColumn: { - name: "courseId", - referencedColumnName: "id", + name: 'courseId', + referencedColumnName: 'id', }, }, }, userCourses: { - type: "one-to-many", - target: "UserCourse", - inverseSide: "user", + type: 'one-to-many', + target: 'UserCourse', + inverseSide: 'user', }, }, indices: [ @@ -70,6 +70,6 @@ const UserEntity = new EntitySchema({ { columns: ['email'] }, { columns: ['role'] }, // Index role if querying by it frequently ], -}) +}); -export default UserEntity \ No newline at end of file +export default UserEntity diff --git a/server.js b/server.js index f5e60c8..ecc3933 100644 --- a/server.js +++ b/server.js @@ -9,14 +9,14 @@ import helmet from 'helmet'; import cors from 'cors'; // Load environment variables -const PORT = process.env.PORT || 3000 +const PORT = process.env.PORT || 5000; // Initialize database connection and start server AppDataSource.initialize() .then(async () => { - console.log("Data Source has been initialized!") + console.log('Database connected'); - const app = express() + const app = express(); // Middleware app.use(helmet()); @@ -28,6 +28,11 @@ AppDataSource.initialize() app.use('/api/jobs', jobRoutes); app.use("/api/courses", courseRoutes) + // Error handling + app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).json({ error: 'Internal server error' }); + }); // Synchronize the entities with the database await AppDataSource.synchronize() app.listen(PORT, () => { From 73488b3502585cdd2bae0a92650e9a031bf22ae1 Mon Sep 17 00:00:00 2001 From: OthmanImam Date: Sat, 1 Feb 2025 01:52:03 -0800 Subject: [PATCH 9/9] resolved the conflict between App.js and Server.js --- app.js | 29 ----------------------------- server.js | 44 ++++++++++++++++++++------------------------ 2 files changed, 20 insertions(+), 53 deletions(-) diff --git a/app.js b/app.js index 0355da3..e69de29 100644 --- a/app.js +++ b/app.js @@ -1,29 +0,0 @@ -import dotenv from "dotenv" -import express from "express" -import AppDataSource from "./config/config.js" - -dotenv.config() - -const app = express() -const PORT = process.env.PORT || 3000 - -app.use(express.json()) - -async function startServer() { - try { - await AppDataSource.initialize() - console.log("Data Source has been initialized!") - - app.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`) - }) - } catch (err) { - console.error("Error during Data Source initialization", err) - process.exit(1) - } -} - -startServer() - -export default app - diff --git a/server.js b/server.js index ecc3933..8e009a3 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,5 @@ +import AppDataSource from "./config/config.js"; import 'dotenv/config' -import AppDataSource from "./config/config.js" import express, { json } from 'express'; import authRouter from './routes/auth.routes.js'; import jobRoutes from './routes/jobRoutes.js'; @@ -8,39 +8,35 @@ import courseRoutes from "./routes/courseRoutes.js" import helmet from 'helmet'; import cors from 'cors'; -// Load environment variables const PORT = process.env.PORT || 5000; -// Initialize database connection and start server AppDataSource.initialize() -.then(async () => { +.then(() => { console.log('Database connected'); const app = express(); - + // Middleware app.use(helmet()); app.use(cors()); app.use(json()); - - // Routes - app.use('/api/auth', authRouter); - app.use('/api/jobs', jobRoutes); - app.use("/api/courses", courseRoutes) - - // Error handling - app.use((err, req, res, next) => { - console.error(err.stack); - res.status(500).json({ error: 'Internal server error' }); - }); - // Synchronize the entities with the database - await AppDataSource.synchronize() - app.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`) - }) + // Routes + app.use('/api/auth', authRouter); + app.use('/api/jobs', jobRoutes); + app.use("/api/courses", courseRoutes) + + // Error handling + app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).json({ error: 'Internal server error' }); + }); + + app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); + }); }) .catch((error) => { - console.error('Database connection failed:', error); - process.exit(1); - }) \ No newline at end of file + console.error('Database connection failed:', error); + process.exit(1); + }); \ No newline at end of file