diff --git a/week-11/solution/.env.example b/week-11/solution/.env.example new file mode 100644 index 0000000000..e2f10c8b78 --- /dev/null +++ b/week-11/solution/.env.example @@ -0,0 +1,2 @@ +DATABASE_URL="" +DIRECT_URL="" \ No newline at end of file diff --git a/week-11/solution/.gitignore b/week-11/solution/.gitignore new file mode 100644 index 0000000000..3c0be6ea77 --- /dev/null +++ b/week-11/solution/.gitignore @@ -0,0 +1,10 @@ +node_modules +dist +.wrangler +.dev.vars + +# Change them to your taste: +package-lock.json +yarn.lock +pnpm-lock.yaml +bun.lockb \ No newline at end of file diff --git a/week-11/solution/README.md b/week-11/solution/README.md new file mode 100644 index 0000000000..cc58e962d8 --- /dev/null +++ b/week-11/solution/README.md @@ -0,0 +1,8 @@ +``` +npm install +npm run dev +``` + +``` +npm run deploy +``` diff --git a/week-11/solution/package.json b/week-11/solution/package.json new file mode 100644 index 0000000000..a9da982744 --- /dev/null +++ b/week-11/solution/package.json @@ -0,0 +1,18 @@ +{ + "scripts": { + "dev": "wrangler dev src/index.ts", + "deploy": "wrangler deploy --minify src/index.ts" + }, + "dependencies": { + "@prisma/client": "^5.9.1", + "@prisma/extension-accelerate": "^0.6.3", + "hono": "^4.0.3", + "jsonwebtoken": "^9.0.2" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240208.0", + "@types/jsonwebtoken": "^9.0.5", + "prisma": "^5.9.1", + "wrangler": "^3.25.0" + } +} diff --git a/week-11/solution/prisma/migrations/20240311164032_init/migration.sql b/week-11/solution/prisma/migrations/20240311164032_init/migration.sql new file mode 100644 index 0000000000..82f2509fad --- /dev/null +++ b/week-11/solution/prisma/migrations/20240311164032_init/migration.sql @@ -0,0 +1,52 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Posts" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "body" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Posts_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Tags" ( + "id" SERIAL NOT NULL, + "tag" TEXT NOT NULL, + + CONSTRAINT "Tags_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_PostsToTags" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "Tags_tag_key" ON "Tags"("tag"); + +-- CreateIndex +CREATE UNIQUE INDEX "_PostsToTags_AB_unique" ON "_PostsToTags"("A", "B"); + +-- CreateIndex +CREATE INDEX "_PostsToTags_B_index" ON "_PostsToTags"("B"); + +-- AddForeignKey +ALTER TABLE "Posts" ADD CONSTRAINT "Posts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_PostsToTags" ADD CONSTRAINT "_PostsToTags_A_fkey" FOREIGN KEY ("A") REFERENCES "Posts"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_PostsToTags" ADD CONSTRAINT "_PostsToTags_B_fkey" FOREIGN KEY ("B") REFERENCES "Tags"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/week-11/solution/prisma/migrations/migration_lock.toml b/week-11/solution/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000000..fbffa92c2b --- /dev/null +++ b/week-11/solution/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 (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/week-11/solution/prisma/schema.prisma b/week-11/solution/prisma/schema.prisma new file mode 100644 index 0000000000..d11c10f24a --- /dev/null +++ b/week-11/solution/prisma/schema.prisma @@ -0,0 +1,36 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + directUrl = env("DIRECT_URL") +} + +model User { + id Int @id @default(autoincrement()) + username String + email String + password String + posts Posts[] +} + +model Posts { + id Int @id @default(autoincrement()) + title String + body String + tags Tags[] + User User @relation(fields: [userId], references: [id]) + userId Int + createdAt DateTime @default(now()) +} + +model Tags { + id Int @id @default(autoincrement()) + tag String @unique + post Posts[] +} \ No newline at end of file diff --git a/week-11/solution/src/controller/postController.ts b/week-11/solution/src/controller/postController.ts new file mode 100644 index 0000000000..d982bc0bb0 --- /dev/null +++ b/week-11/solution/src/controller/postController.ts @@ -0,0 +1,236 @@ +import { PrismaClient } from '@prisma/client/edge'; +import { withAccelerate } from '@prisma/extension-accelerate'; + +import { Context } from 'hono'; + +enum StatusCode { + BADREQ = 400, + NOTFOUND = 404, + NOTPERMISSIOON = 403, +} + +export async function getPosts(c: Context) { + const prisma = new PrismaClient({ + datasourceUrl: c.env.DATABASE_URL, + }).$extends(withAccelerate()); + + try { + const response = await prisma.posts.findMany({ + include: { + tags: true, + User: true, + }, + }); + return c.json({ + post: response.map((res) => ({ + id: res.id, + username: res.User.username, + userId: res.User.id, + title: res.title, + body: res.body, + tags: res.tags, + createdAt: res.createdAt, + })), + }); + } catch (error) { + return c.body(`Internal server error: ${error}`, 500); + } +} + +export async function getUserPosts(c: Context) { + const prisma = new PrismaClient({ + datasourceUrl: c.env.DATABASE_URL, + }).$extends(withAccelerate()); + + try { + const resp = await prisma.posts.findMany({ + where: { + userId: c.get('userId'), + }, + }); + return c.json({ + post: resp, + }); + } catch (error) { + return c.body(`Internal server error: ${error}`, 500); + } +} + +export async function createPost(c: Context) { + const prisma = new PrismaClient({ + datasourceUrl: c.env.DATABASE_URL, + }).$extends(withAccelerate()); + + try { + const body: { + title: string; + body: string; + tags: string; + } = await c.req.json(); + + const tagNames = body.tags.split(',').map((tag) => tag.trim()); + + if ((body.body && body.title) == null) { + return c.body('Invalid user input', StatusCode.BADREQ); + } + const res = await prisma.posts.create({ + data: { + title: body.title, + body: body.body, + userId: c.get('userId'), + tags: { + connectOrCreate: tagNames.map((tag) => ({ + where: { tag }, + create: { tag }, + })), + }, + }, + include: { + tags: true, + }, + }); + + return c.json({ + message: 'Post successfully', + post: { + id: res.id, + title: res.title, + body: res.body, + tags: res.tags.map((tag) => tag.tag), + createdAt: res.createdAt, + }, + }); + } catch (error) { + return c.body(`Internal server error: ${error}`, 500); + } +} + +export async function getPost(c: Context) { + const prisma = new PrismaClient({ + datasourceUrl: c.env.DATABASE_URL, + }).$extends(withAccelerate()); + + try { + const id: number = Number(c.req.param('id')); + + const isPostExist = await prisma.posts.findFirst({ + where: { + id: id, + userId: c.get('userId'), + }, + include: { + tags: true, + }, + }); + + if (isPostExist == null) { + return c.body('Post does not exists', StatusCode.NOTFOUND); + } + return c.json({ + data: { + id: isPostExist.id, + title: isPostExist.title, + body: isPostExist.body, + tags: isPostExist.tags, + createdAt: isPostExist.createdAt, + }, + }); + } catch (error) { + return c.body(`Internal server error: ${error}`, 500); + } +} + +// this controller update the specific post +export async function updatePost(c: Context) { + const prisma = new PrismaClient({ + datasourceUrl: c.env.DATABASE_URL, + }).$extends(withAccelerate()); + + try { + const id: number = Number(c.req.param('id')); + + const body: { + title: string; + body: string; + tags: string; + } = await c.req.json(); + + const tagNames = body.tags.split(',').map((tag) => tag.trim()); + + const isPostExist = await prisma.posts.findFirst({ + where: { + id: id, + userId: c.get('userId'), + }, + }); + + if (isPostExist == null) { + return c.body('Post does not exists', StatusCode.NOTFOUND); + } + + const res = await prisma.posts.update({ + where: { + id: id, + userId: c.get('userId'), + }, + data: { + title: body.title, + body: body.body, + tags: { + connectOrCreate: tagNames.map((tag) => ({ + where: { tag }, + create: { tag }, + })), + }, + }, + include: { + tags: true, + }, + }); + + return c.json({ + data: { + id: res.id, + title: res.title, + body: res.body, + tags: res.tags, + createdAt: res.createdAt, + }, + }); + } catch (error) { + return c.body(`Internal server error: ${error}`, 500); + } +} + +export async function deletePost(c: Context) { + const prisma = new PrismaClient({ + datasourceUrl: c.env.DATABASE_URL, + }).$extends(withAccelerate()); + + try { + const id: number = Number(c.req.param('id')); + + const isPostExist = await prisma.posts.findFirst({ + where: { + id: id, + userId: c.get('userId'), + }, + }); + + if (isPostExist == null) { + return c.body('Post does not exists', StatusCode.NOTFOUND); + } + + const res = await prisma.posts.delete({ + where: { + id: id, + userId: c.get('userId'), + }, + }); + return c.json({ + message: 'post deleted', + }); + } catch (error) { + return c.json({ msg: `Internal server error: ${error}` }, 500); + } +} diff --git a/week-11/solution/src/controller/tagController.ts b/week-11/solution/src/controller/tagController.ts new file mode 100644 index 0000000000..b1bdcb3772 --- /dev/null +++ b/week-11/solution/src/controller/tagController.ts @@ -0,0 +1,65 @@ +import { PrismaClient } from '@prisma/client/edge'; +import { withAccelerate } from '@prisma/extension-accelerate'; + +import { Context } from 'hono'; + +enum StatusCode { + BADREQ = 400, + NOTFOUND = 404, + NOTPERMISSIOON = 403, +} + +export const getTags = async (c: Context) => { + const prisma = new PrismaClient({ + datasourceUrl: c.env.DATABASE_URL, + }).$extends(withAccelerate()); + + try { + const res = await prisma.tags.findMany(); + + return c.json({ + tags: res, + }); + } catch (error) { + return c.body(`Internal server error: ${error}`, 500); + } +}; + +export const getPostsByTag = async (c: Context) => { + const prisma = new PrismaClient({ + datasourceUrl: c.env.DATABASE_URL, + }).$extends(withAccelerate()); + + try { + const res = await prisma.tags.findMany({ + where: { + tag: String(c.req.param('tag')), + }, + select: { + post: { + select: { + User: { select: { username: true } }, + id: true, + userId: true, + title: true, + body: true, + createdAt: true, + }, + }, + }, + }); + + return c.json({ + posts: res[0].post.map((post) => ({ + username: post.User.username, + id: post.id, + title: post.title, + userId: post.userId, + body: post.body, + createdAt: post.createdAt, + })), + }); + } catch (error) { + return c.body(`Internal server error: ${error}`, 500); + } +}; diff --git a/week-11/solution/src/controller/userController.ts b/week-11/solution/src/controller/userController.ts new file mode 100644 index 0000000000..3d6f8f1288 --- /dev/null +++ b/week-11/solution/src/controller/userController.ts @@ -0,0 +1,160 @@ +import { PrismaClient } from '@prisma/client/edge'; +import { withAccelerate } from '@prisma/extension-accelerate'; +import { signinSchema, signupSchema } from '../zod/user'; +import { Jwt } from 'hono/utils/jwt'; +import { Context } from 'hono'; + +enum StatusCode { + BADREQ = 400, + NOTFOUND = 404, + NOTPERMISSIOON = 403, +} + +export async function signup(c: Context) { + const prisma = new PrismaClient({ + datasourceUrl: c.env.DATABASE_URL, + }).$extends(withAccelerate()); + + try { + const body: { + username: string; + email: string; + password: string; + } = await c.req.json(); + + const parsedUser = signupSchema.safeParse(body); + + if (!parsedUser.success) { + return c.body('Invalid user input', StatusCode.BADREQ); + } + + const isUserExist = await prisma.user.findFirst({ + where: { email: body.email }, + }); + + if (isUserExist) { + return c.body('email already exist', StatusCode.BADREQ); + } + + const res = await prisma.user.create({ + data: { + username: body.username, + email: body.email, + password: body.password, + }, + }); + + const userId = res.id; + + const token = await Jwt.sign(userId, c.env.JWT_TOKEN); + + return c.json({ + msg: 'User created successfully', + token: token, + user: { + userId: res.id, + username: res.username, + email: res.email, + }, + }); + } catch (error) { + return c.body(`Internal server error: ${error}`, 500); + } +} + +export async function signin(c: Context) { + const prisma = new PrismaClient({ + datasourceUrl: c.env.DATABASE_URL, + }).$extends(withAccelerate()); + + try { + const body: { + email: string; + password: string; + } = await c.req.json(); + + const parsedUser = signinSchema.safeParse(body); + + if (!parsedUser.success) { + return c.body('Invalid user input', StatusCode.BADREQ); + } + + const isUserExist = await prisma.user.findFirst({ + where: { + email: body.email, + password: body.password, + }, + }); + + if (isUserExist == null) { + return c.body('User does not exists', StatusCode.BADREQ); + } + + const userId = isUserExist.id; + + const token = await Jwt.sign(userId, c.env.JWT_TOKEN); + + return c.json({ + message: 'login successfully', + token: token, + user: { + userId: userId, + username: isUserExist.username, + email: isUserExist.email, + }, + }); + } catch (error) { + return c.body(`Internal server error: ${error}`, 500); + } +} + +export async function userProfile(c: Context) { + const prisma = new PrismaClient({ + datasourceUrl: c.env.DATABASE_URL, + }).$extends(withAccelerate()); + + try { + const res = await prisma.user.findFirst({ + where: { + id: Number(c.req.param('id')), + }, + include: { + posts: true, + }, + }); + + if (res == null) { + return c.body('User not found', 404); + } else { + return c.json({ + user: { + id: res.id, + username: res.username, + email: res.email, + posts: res.posts, + }, + }); + } + } catch (error) { + return c.body(`Internal server error: ${error}`, 500); + } +} + +export const getAllUsers = async (c: Context) => { + const prisma = new PrismaClient({ + datasourceUrl: c.env.DATABASE_URL, + }).$extends(withAccelerate()); + + try { + const res = await prisma.user.findMany(); + return c.json({ + users: res.map((user) => ({ + id: user.id, + username: user.username, + email: user.email, + })), + }); + } catch (error) { + return c.body(`Internal server error: ${error}`, 500); + } +}; diff --git a/week-11/solution/src/index.ts b/week-11/solution/src/index.ts new file mode 100644 index 0000000000..51ef8660fc --- /dev/null +++ b/week-11/solution/src/index.ts @@ -0,0 +1,14 @@ +import { Hono } from 'hono'; +import { userRouter } from './router/userRoutes'; +import { cors } from 'hono/cors'; +import { postRouter } from './router/postRoutes'; +import { tagRouter } from './router/tagRoutes'; +const app = new Hono(); + +app.use(cors()); + +app.route('/api/v1/user', userRouter); +app.route('/api/v1/posts', postRouter); +app.route('/api/v1/tags', tagRouter); + +export default app; diff --git a/week-11/solution/src/middleware/user.ts b/week-11/solution/src/middleware/user.ts new file mode 100644 index 0000000000..f50a8c564b --- /dev/null +++ b/week-11/solution/src/middleware/user.ts @@ -0,0 +1,24 @@ +import { Context, Next } from "hono"; +import { env } from "hono/adapter"; +import { Jwt } from "hono/utils/jwt"; + +export async function authmiddleware(c: any, next: Next) { + const JWT_TOKEN = "mytoken"; + + try { + const token: string = c.req.header("Authorization").split(" ")[1]; + if (token !== null || token !== undefined) { + const decode = await Jwt.verify(token, JWT_TOKEN); + if (decode) { + c.set("userId", decode); + await next(); + } else { + return c.body("you are unauthroized user sorry", 401); + } + } else { + return c.body("you are unauthroized user", 401); + } + } catch (error) { + return c.body("unauthroized ", 401); + } +} diff --git a/week-11/solution/src/router/postRoutes.ts b/week-11/solution/src/router/postRoutes.ts new file mode 100644 index 0000000000..ddbf7a857c --- /dev/null +++ b/week-11/solution/src/router/postRoutes.ts @@ -0,0 +1,19 @@ +import { Context, Hono } from 'hono'; +import { + createPost, + deletePost, + getPost, + getPosts, + getUserPosts, + updatePost, +} from '../controller/postController'; +import { authmiddleware } from '../middleware/user'; + +export const postRouter = new Hono(); + +postRouter.get('/all-posts', getPosts); +postRouter.get('/posts', authmiddleware, getUserPosts); +postRouter.post('/create-post', authmiddleware, createPost); +postRouter.get('/post/:id', authmiddleware, getPost); +postRouter.put('/post/:id', authmiddleware, updatePost); +postRouter.delete('/post/:id', authmiddleware, deletePost); diff --git a/week-11/solution/src/router/tagRoutes.ts b/week-11/solution/src/router/tagRoutes.ts new file mode 100644 index 0000000000..10af2ba06e --- /dev/null +++ b/week-11/solution/src/router/tagRoutes.ts @@ -0,0 +1,8 @@ +import { Hono } from 'hono'; + +import { getPostsByTag, getTags } from '../controller/tagController'; + +export const tagRouter = new Hono(); + +tagRouter.get('/getPost/:tag', getPostsByTag); +tagRouter.get('/tags', getTags); diff --git a/week-11/solution/src/router/userRoutes.ts b/week-11/solution/src/router/userRoutes.ts new file mode 100644 index 0000000000..7eb9685367 --- /dev/null +++ b/week-11/solution/src/router/userRoutes.ts @@ -0,0 +1,16 @@ +import { Hono } from 'hono'; +import { + getAllUsers, + signin, + signup, + userProfile, +} from '../controller/userController'; +import { authmiddleware } from '../middleware/user'; + +export const userRouter = new Hono(); + +userRouter.post('/signup', signup); +userRouter.post('/signin', signin); + +userRouter.get('/user/:id', authmiddleware, userProfile); +userRouter.get('/users', authmiddleware, getAllUsers); diff --git a/week-11/solution/src/zod/user.ts b/week-11/solution/src/zod/user.ts new file mode 100644 index 0000000000..4357e325df --- /dev/null +++ b/week-11/solution/src/zod/user.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +export const signupSchema = z.object({ + username: z.string(), + email: z.string().email(), + password: z.string(), +}); + +export const signinSchema = z.object({ + email: z.string().email(), + password: z.string(), +}); diff --git a/week-11/solution/tsconfig.json b/week-11/solution/tsconfig.json new file mode 100644 index 0000000000..33a96fd088 --- /dev/null +++ b/week-11/solution/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "lib": [ + "ESNext" + ], + "types": [ + "@cloudflare/workers-types" + ], + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + }, +} \ No newline at end of file diff --git a/week-11/solution/wrangler.toml b/week-11/solution/wrangler.toml new file mode 100644 index 0000000000..30ac9126f8 --- /dev/null +++ b/week-11/solution/wrangler.toml @@ -0,0 +1,21 @@ +name = "-" +compatibility_date = "2023-12-01" + +# [vars] +# MY_VARIABLE = "production_value" + +DATABASE_URL="" +JWT_TOKEN = "" + +# [[kv_namespaces]] +# binding = "MY_KV_NAMESPACE" +# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +# [[r2_buckets]] +# binding = "MY_BUCKET" +# bucket_name = "my-bucket" + +# [[d1_databases]] +# binding = "DB" +# database_name = "my-database" +# database_id = ""