diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b7d3400..37d92c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,5 +28,3 @@ jobs: - name: Run build run: npm run build - - name: Create certificate - run: npm run generate-ssl diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e76fe82..676ca8d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,4 +41,4 @@ jobs: sudo docker pull adil56/my-uni-backend:latest sudo docker stop my-uni-backend || true sudo docker rm my-uni-backend || true - sudo docker run -d --name my-uni-backend -p 8080:8000 adil56/my-uni-backend:latest + sudo docker run -d --name my-uni-backend -p 8080:8000 --env-file /home/ubuntu/.env.docker adil56/my-uni-backend:latest diff --git a/Dockerfile b/Dockerfile index 8cacc5c..dd594a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ FROM node:20-slim +RUN apt-get update -y && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* WORKDIR /src @@ -7,8 +8,6 @@ RUN npm install COPY . . -COPY ./src/config/certs ./dist/config/certs - RUN npm run db:generate RUN npm run build diff --git a/README.md b/README.md index 96feb0b..2aaadb2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ - delete the "graphql" dependency from the package.json - npm install - npm install graphql -- npm run generate-ssl // for generating instance in dist folder - npm run db:migrate // migrating all prisma model - npx prisam migrate reset // reset all prisma model - npm run db:generate // generate all models in postgress-db diff --git a/package.json b/package.json index 32a57fc..0eb7d88 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "dev": "tsc-watch --onSuccess \"npm start\" ", "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1", - "generate-ssl": "mkdir -p dist/config/certs && cp -r src/config/certs/* dist/config/certs/", "db:generate": "npx prisma generate", "db:migrate": "npx prisma migrate deploy", "db:push": "npx prisma db push", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b0e5357..7e39fcc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -57,12 +57,13 @@ model User { createdAt DateTime @default(now()) // Relations - courseData CourseData[] @relation("UserToProgramData") - programData ProgramData[] @relation("programsByUser") - semesterData SemesterData[] @relation("semesterByuser") - DepartmentData DepartmentData[] @relation("UserToDepartmentData") - AnnouncementData AnnouncementData[] @relation("announcementByUser") - EventsData EventsData[] @relation("eventsByUser") + courseData CourseData[] @relation("UserToProgramData") + programData ProgramData[] @relation("programsByUser") + semesterData SemesterData[] @relation("semesterByuser") + DepartmentData DepartmentData[] @relation("UserToDepartmentData") + AnnouncementData AnnouncementData[] @relation("announcementByUser") + EventsData EventsData[] @relation("eventsByUser") + ContentManagement ContentManagement[] @relation("articleByUser") @@map("users") } @@ -88,12 +89,12 @@ model CourseData { } model DepartmentData { - id String @id @default(uuid()) - userId String - name String - school String + id String @id @default(uuid()) + userId String + name String + school String - head String + head String established DateTime @db.Date createdAt DateTime @default(now()) @@ -122,13 +123,13 @@ model ProgramData { } model SemesterData { - id String @id @default(uuid()) + id String @id @default(uuid()) userId String semester String - startdate DateTime @db.Date - enddate DateTime @db.Date + startdate DateTime @db.Date + enddate DateTime @db.Date - courses String + courses String status SemesterStatus @default(Planning) createdAt DateTime @default(now()) @@ -183,3 +184,46 @@ model EventsData { createdBy User @relation("eventsByUser", fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) createdAt DateTime @default(now()) } + +// ======= content-management ======= // + +enum ArticleCategory { + ACADEMIC + EVENTS + FACILITIES + NEWS + ANNOUNCEMENTS +} + +enum ArticleStatus { + DRAFT + PUBLISHED +} + +model ContentManagement { + id String @id @default(uuid()) + title String + userId String + subtitle String? + slug String? @unique + content String @db.Text + coverimage String? + category ArticleCategory @default(NEWS) + status ArticleStatus @default(DRAFT) + + metaTitle String? + metaDescription String? + keywords String[] + focusKeyword String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + publishedAt DateTime? @default(now()) + + author User @relation("articleByUser", fields: [userId], references: [id], onDelete: Cascade) + + @@index([status, publishedAt]) + @@index([category, status]) + @@index([userId]) + @@map("content_management") +} diff --git a/src/config/types/content-management.ts b/src/config/types/content-management.ts new file mode 100644 index 0000000..d7d49c1 --- /dev/null +++ b/src/config/types/content-management.ts @@ -0,0 +1,32 @@ +import z from "zod"; + +export const ContentManagementFileds = z.object({ + title: z.string().min(3, { message: "title is required" }), + subtitle: z.string().optional(), + userId: z.string(), + slug: z.string().optional(), + content: z + .string() + .min(10, { message: "Content is required, minimum 10 words" }), + coverimage: z.string().optional(), + status: z.enum(["PUBLISHED", "DRAFT"]), + category: z.enum([ + "ACADEMIC", + "EVENTS", + "FACILITIES", + "NEWS", + "ANNOUNCEMENTS", + ]), + + metaTitle: z.string().optional(), + metaDescription: z.string().optional(), + keywords: z.array(z.string()).optional(), + focusKeyword: z.string().optional(), +}); + +export const updateContentMenagementField = ContentManagementFileds.partial(); + +type ContentManagement = z.infer; +type updateContentManagement = z.infer< + typeof updateContentMenagementField +>; diff --git a/src/controllers/announcement/announcement.ts b/src/controllers/announcement/announcement.ts index 0c2a467..28b4430 100644 --- a/src/controllers/announcement/announcement.ts +++ b/src/controllers/announcement/announcement.ts @@ -45,7 +45,7 @@ export class AnnouncementController { announcementData, 3600 ); - await delCache("announcement:all"); // for removing old cache -- + await delCache("announcement:all"); res.status(200).json({ message: "announcement created !" }); return; diff --git a/src/controllers/auth/google/account.ts b/src/controllers/auth/google/account.ts index 7de3dde..8675b84 100644 --- a/src/controllers/auth/google/account.ts +++ b/src/controllers/auth/google/account.ts @@ -3,7 +3,7 @@ import { Strategy as GoogleStrategy } from "passport-google-oauth2"; import { PrismaClient } from "@prisma/client"; import { Request } from "express"; import { WelcomeEmail } from "../../../services/email-service"; -import { getCache, setCache } from "../../../services/cached/cached-data"; +import { setCache } from "../../../services/cached/cached-data"; const prisma = new PrismaClient(); diff --git a/src/controllers/content-management/content-management.ts b/src/controllers/content-management/content-management.ts new file mode 100644 index 0000000..5dbcb35 --- /dev/null +++ b/src/controllers/content-management/content-management.ts @@ -0,0 +1,182 @@ +import { PrismaClient } from "@prisma/client"; +import { NextFunction, Request, Response } from "express"; +import { + delCache, + getCache, + setCache, +} from "../../services/cached/cached-data"; +import { uploadToCloudinary } from "../../lib/cloudnary/uploadToCloudinary"; + +export class Contentmanagement { + prisma = new PrismaClient(); + + // create blog + async createArticle( + req: Request, + res: Response, + next: NextFunction + ): Promise { + try { + const { + title, + subtitle, + slug, + content, + status, + category, + metaTitle, + metaDescription, + keywords, + focusKeyword, + userId, + } = req.body; + + let coverimage: string | null = null; + + if (req.file) { + coverimage = await uploadToCloudinary(req.file.buffer, "articles"); + } + + const createdArticle = await this.prisma.contentManagement.create({ + data: { + coverimage, + title, + subtitle, + slug, + userId, + content, + status, + category, + metaTitle, + metaDescription, + keywords, + focusKeyword, + }, + }); + + await setCache(`article:${createdArticle.id}`, createdArticle, 3600); + await delCache("article:all"); + + res.status(200).json({ message: "article created !" }); + } catch (error) { + next(error.message); + } + } + + // read blog + async getAllArticles( + req: Request, + res: Response, + next: NextFunction + ): Promise { + try { + const cachedKey = "article:all"; + const cachedArticle = await getCache(cachedKey); + + if (cachedArticle) { + res.status(200).json({ + message: "article fetched (cache)", + AllArticles: cachedArticle, + }); + return; + } + + const AllArticles = await this.prisma.contentManagement.findMany({ + select: { + id: true, + coverimage: true, + title: true, + subtitle: true, + slug: true, + author: { + select: { + firstname: true, + lastname: true, + }, + }, + content: true, + status: true, + category: true, + metaTitle: true, + metaDescription: true, + keywords: true, + focusKeyword: true, + }, + }); + + if (AllArticles.length > 0) { + res.status(200).json({ message: "Articles Data", AllArticles }); + return; + } + + res.status(200).json({ message: "Not any articles Data exist to show" }); + return; + } catch (error) { + next(error.message); + } + } + // update blog + + async updateArticle( + req: Request, + res: Response, + next: NextFunction + ): Promise { + try { + const { id } = req.params; + const filedsToUpdate = req.body.data ?? req.body; + + const isArticleExist = await this.prisma.contentManagement.findFirst({ + where: { id: id }, + }); + + if (!isArticleExist) { + res.status(401).json({ message: "Article not found for update" }); + return; + } + + const updatedArticles = await this.prisma.contentManagement.update({ + where: { id: id }, + data: filedsToUpdate, + }); + + await setCache(`article:${id}`, updatedArticles, 3600); + await delCache("article:all"); + + res.status(200).json({ message: "article created !", updatedArticles }); + return; + } catch (error) {} + } + + // delete blog + + async deleteArticle( + req: Request, + res: Response, + next: NextFunction + ): Promise { + try { + const { id } = req.params; + + const isArticleExist = await this.prisma.contentManagement.findFirst({ + where: { id: id }, + }); + + if (!isArticleExist) { + res.status(401).json({ message: "article not founce to delete" }); + return; + } + + await this.prisma.contentManagement.delete({ + where: { id: id }, + }); + + await delCache(`article:${id}`); + await delCache("article:all"); // for removing old cache -- + + res.status(200).json({ message: "article deleted" }); + } catch (error) { + next(error.message); + } + } +} diff --git a/src/routes/auth/google-account.ts b/src/routes/auth/google-account.ts index e0ac5a8..6d29f70 100644 --- a/src/routes/auth/google-account.ts +++ b/src/routes/auth/google-account.ts @@ -48,8 +48,8 @@ router.get( const redirectUser = user.role === "Admin" - ? "https://localhost:5174" - : "https://localhost:5173"; + ? "http://localhost:5174" + : "http://localhost:5173"; req.session.save((err) => { if (err) { diff --git a/src/routes/content-management/content-management.ts b/src/routes/content-management/content-management.ts new file mode 100644 index 0000000..041812d --- /dev/null +++ b/src/routes/content-management/content-management.ts @@ -0,0 +1,59 @@ +import express from "express"; +import { userId } from "../../lib/middlewares/use-user-id"; +import { Contentmanagement } from "../../controllers/content-management/content-management"; +import { check_user_logged_in } from "../../lib/middlewares/check-user-logged-in"; +import { isUserAdmin } from "../../lib/middlewares/is-user-admin"; +import { isUserVerified } from "../../lib/middlewares/is-user-verified"; +import { verifyInputFields } from "../../lib/middlewares/verify-fileds"; +import { + ContentManagementFileds, + updateContentMenagementField, +} from "../../config/types/content-management"; +import upload from "../../lib/middlewares/multer"; + +const router = express.Router(); +const ContentmanagementController = new Contentmanagement(); + +router + .route("/api/v1/create-article") + .post( + check_user_logged_in, + isUserAdmin, + isUserVerified, + userId, + upload.single("coverimage"), + verifyInputFields(ContentManagementFileds), + ContentmanagementController.createArticle.bind(ContentmanagementController) + ); + +router + .route("/api/v1/get-all-articles") + .get( + check_user_logged_in, + isUserAdmin, + isUserVerified, + ContentmanagementController.getAllArticles.bind(ContentmanagementController) + ); + +router + .route("/api/v1/update-article/:id") + .put( + check_user_logged_in, + isUserAdmin, + isUserVerified, + userId, + upload.single("coverimage"), + verifyInputFields(updateContentMenagementField), + ContentmanagementController.updateArticle.bind(ContentmanagementController) + ); + +router + .route("/api/v1/delete-article/:id") + .delete( + check_user_logged_in, + isUserAdmin, + isUserVerified, + ContentmanagementController.deleteArticle.bind(ContentmanagementController) + ); + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index 354d4ac..d6b2c26 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -37,6 +37,9 @@ import Announcement from "./announcement/announcement"; import Events from "./events/events"; +// content-management +import ContentManagement from "./content-management/content-management"; + const router = Router(); // Grouped by path prefix @@ -81,4 +84,7 @@ router.use(Announcement); // ---- events ---- // router.use(Events); +// ----- conetnt-management ---- // +router.use(ContentManagement); + export default router;