Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…o-api into master
  • Loading branch information
mariatorrentedev committed Jun 10, 2024
2 parents f794507 + 5feec96 commit 4c45599
Show file tree
Hide file tree
Showing 17 changed files with 221 additions and 10 deletions.
10 changes: 7 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"typescript-eslint": "^7.11.0"
},
"dependencies": {
"@prisma/client": "^5.14.0",
"@prisma/client": "^5.15.0",
"bcryptjs": "^2.4.3",
"express": "^4.19.2",
"helmet": "^7.1.0",
Expand Down
26 changes: 26 additions & 0 deletions prisma/migrations/20240610171246_blog/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- CreateTable
CREATE TABLE "BlogPost" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"authorId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "BlogPost_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "Subscriber" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "Subscriber_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "Subscriber_email_key" ON "Subscriber"("email");

-- AddForeignKey
ALTER TABLE "BlogPost" ADD CONSTRAINT "BlogPost_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
19 changes: 18 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,24 @@ model User {
id Int @id @default(autoincrement())
email String @unique
password String
role Role @default(USER)
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authoredPosts BlogPost[]
}

model BlogPost {
id Int @id @default(autoincrement())
title String
content String @db.Text //Ensure large string data type.
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Subscriber {
id Int @id @default(autoincrement())
email String @unique
createdAt DateTime @default(now())
}
4 changes: 2 additions & 2 deletions src/controllers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Request, Response } from "express";
import type { Request, Response } from "express";
import * as authService from "../services/auth";
import { composeError } from "../utils";

export async function signup(req: Request, res: Response) {
export async function signUp(req: Request, res: Response) {
const { email, password } = req.body;
try {
const user = await authService.signup(email, password);
Expand Down
64 changes: 64 additions & 0 deletions src/controllers/blog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Request, Response } from "express";
import { BlogService } from "../services/blog";
import { composeError } from "../utils";

export const createBlogPost = async (req: Request, res: Response) => {
try {
const { title, content, authorId } = req.body;
const newBlogPost = await BlogService.createBlogPost({
title,
content,
authorId,
});
res.status(201).json(newBlogPost);
} catch (error) {
res.status(500).json({ error: composeError(error) });
}
};

export const getBlogPosts = async (req: Request, res: Response) => {
try {
const blogPosts = await BlogService.getBlogPosts();
res.json(blogPosts);
} catch (error) {
res.status(500).json({ error: composeError(error) });
}
};

export const getBlogPostById = async (req: Request, res: Response) => {
try {
const postId = parseInt(req.params.id);
const blogPost = await BlogService.getBlogPostById(postId);
if (!blogPost) {
res.status(404).json({ error: "Blog post not found" });
} else {
res.json(blogPost);
}
} catch (error) {
res.status(500).json({ error: composeError(error) });
}
};

export const updateBlogPost = async (req: Request, res: Response) => {
try {
const postId = parseInt(req.params.id);
const { title, content } = req.body;
const updatedPost = await BlogService.updateBlogPost(postId, {
title,
content,
});
res.json(updatedPost);
} catch (error) {
res.status(500).json({ error: composeError(error) });
}
};

export const deleteBlogPost = async (req: Request, res: Response) => {
try {
const postId = parseInt(req.params.id);
await BlogService.deleteBlogPost(postId);
res.sendStatus(204);
} catch (error) {
res.status(500).json({ error: composeError(error) });
}
};
19 changes: 18 additions & 1 deletion src/controllers/user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Request, Response } from "express";
import type { Request, Response } from "express";
import * as userService from "../services/user";
import { composeError } from "../utils";
import bcrypt from "bcryptjs";

export async function getUsers(req: Request, res: Response) {
try {
Expand Down Expand Up @@ -34,3 +35,19 @@ export async function deleteUserById(req: Request, res: Response) {
res.status(500).json({ error: composeError(error) });
}
}

export async function editUser(req: Request, res: Response) {
const userId = parseInt(req.params.id);
const { email, password, ...otherProps } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
try {
const updatedUser = await userService.editUser(userId, {
email,
password: hashedPassword,
...otherProps,
});
res.json(updatedUser);
} catch (error) {
res.status(500).json({ error: composeError(error) });
}
}
8 changes: 8 additions & 0 deletions src/middleware/authToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,21 @@ export const authenticateToken = async (
if (!token) return res.sendStatus(401);

try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const decoded: any = jwt.verify(token, SECRET_KEY);
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
});
if (!user) {
return res.sendStatus(401);
}

// Check if user has ADMIN role
if (user.role !== "ADMIN") {
// Forbidden if not ADMIN
return res.sendStatus(403);
}

req.user = user;
next();
} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as authController from "../controllers/auth";

const router = Router();

router.post("/signup", authController.signup);
router.post("/signup", authController.signUp);
router.post("/login", authController.login);

export default router;
16 changes: 16 additions & 0 deletions src/routes/blog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import express from "express";
import * as blogController from "../controllers/blog";
import { authenticateToken } from "../middleware/authToken";

const router = express.Router();

// Private routes
router.post("/", authenticateToken, blogController.createBlogPost);
router.put("/:id", authenticateToken, blogController.updateBlogPost);
router.delete("/:id", authenticateToken, blogController.deleteBlogPost);

// Public routes
router.get("/", blogController.getBlogPosts);
router.get("/:id", blogController.getBlogPostById);

export default router;
1 change: 1 addition & 0 deletions src/routes/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ const router = Router();
router.get("/", authenticateToken, userController.getUsers);
router.get("/:id", authenticateToken, userController.getUserById);
router.delete("/:id", authenticateToken, userController.deleteUserById);
router.put("/:id", authenticateToken, userController.editUser);

export default router;
2 changes: 2 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import helmet from "helmet";
import { PORT, NODE_ENV } from "./config";
import userRoutes from "./routes/user";
import authRoutes from "./routes/auth";
import blogRoutes from "./routes/blog";

// The minimal output for production, Standard Apache for dev.
const morganOption = NODE_ENV === "production" ? "tiny" : "common";
Expand All @@ -19,6 +20,7 @@ app.use(helmet());
// Routes
app.use("/api/users", userRoutes);
app.use("/api/auth", authRoutes);
app.use("/api/blog", blogRoutes);

app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
Expand Down
2 changes: 1 addition & 1 deletion src/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export async function signup(email: string, password: string) {
export async function login(email: string, password: string) {
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
throw new Error("Invalid email or password");
throw new Error("No User Found: invalid email or password");
}

const validPassword = await bcrypt.compare(password, user.password);
Expand Down
36 changes: 36 additions & 0 deletions src/services/blog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { PrismaClient } from "@prisma/client";
import { BlogPost } from "../types/blog";

const prisma = new PrismaClient();

export const BlogService = {
async createBlogPost(blogPostData: Omit<BlogPost, "id">): Promise<BlogPost> {
const newBlogPost = await prisma.blogPost.create({ data: blogPostData });
return newBlogPost;
},

async getBlogPosts(): Promise<BlogPost[]> {
const blogPosts = await prisma.blogPost.findMany();
return blogPosts;
},

async getBlogPostById(id: number): Promise<BlogPost | null> {
const blogPost = await prisma.blogPost.findUnique({ where: { id } });
return blogPost;
},

async updateBlogPost(
id: number,
updatedData: Partial<BlogPost>
): Promise<BlogPost> {
const updatedPost = await prisma.blogPost.update({
where: { id },
data: updatedData,
});
return updatedPost;
},

async deleteBlogPost(id: number): Promise<void> {
await prisma.blogPost.delete({ where: { id } });
},
};
11 changes: 11 additions & 0 deletions src/services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,14 @@ export async function getUserById(id: number) {
export async function deleteUserById(id: number) {
return prisma.user.delete({ where: { id } });
}

export async function editUser(
userId: number,
data: { email: string; password: string }
) {
const updatedUser = await prisma.user.update({
where: { id: userId },
data,
});
return updatedUser;
}
8 changes: 8 additions & 0 deletions src/types/blog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type BlogPost = {
id: number;
title: string;
content: string;
authorId: number;
createdAt?: Date;
updatedAt?: Date;
};
1 change: 1 addition & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const composeError = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any,
customMessage: string = "An unexpected error occurred"
) => {
Expand Down

0 comments on commit 4c45599

Please sign in to comment.