From be6d5c78259bfed2f4be44f37e0d32edf76c4eb6 Mon Sep 17 00:00:00 2001 From: salmanabdurrahman Date: Wed, 15 Oct 2025 20:27:54 +0700 Subject: [PATCH 1/4] feat(config): add documentation url to environment variables - add DOCS_URL to .env.example - read DOCS_URL from environment variables using config - pass documentationUrl to welcome view from config --- .env.example | 3 +++ src/api/welcome/welcome.controller.ts | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 3eb9135..c667b81 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,9 @@ # Application PORT=3000 +# Documentation URL +DOCS_URL=YOUR_DOCUMENTATION_URL_HERE + # JWT JWT_SECRET=YOUR_RANDOM_SECRET_HERE diff --git a/src/api/welcome/welcome.controller.ts b/src/api/welcome/welcome.controller.ts index a0d4936..8ce017b 100644 --- a/src/api/welcome/welcome.controller.ts +++ b/src/api/welcome/welcome.controller.ts @@ -1,7 +1,8 @@ import type { Request, Response } from 'express'; +import config from '../../config/index.js'; export function getWelcomeHandler(_req: Request, res: Response) { res.render('web/index', { - documentationUrl: 'https://documenter.getpostman.com/view/38960737/2sB3QJQBZ8', + documentationUrl: config.docsUrl, }); } From f175b2e5776d5c9029c437c29727e1fbcaec9321 Mon Sep 17 00:00:00 2001 From: salmanabdurrahman Date: Wed, 15 Oct 2025 20:28:42 +0700 Subject: [PATCH 2/4] feat(config): add docs url to configuration - add docs url to config file - update readme with docs url explanation --- README.md | 1 + src/config/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index b5831ff..86bc2b7 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ Sebelum mulai, pastikan sudah install: File `.env` digunakan untuk mengkonfigurasi aplikasi. Berikut adalah penjelasan untuk setiap variabel yang ada di `.env.example`: - `PORT`: Port tempat server akan berjalan (contoh: `3000`). +- `DOCS_URL`: URL untuk dokumentasi API (contoh: `/docs`). - `JWT_SECRET`: Kunci rahasia acak untuk menandatangani token JWT. - `GOOGLE_CLIENT_ID`: Client ID dari Google Cloud Console untuk otentikasi Google OAuth. - `GEMINI_API_KEY`: Kunci API untuk layanan Google Gemini yang digunakan oleh AI Coach. diff --git a/src/config/index.ts b/src/config/index.ts index 36fc305..72b6260 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -5,6 +5,7 @@ dotenv.config(); const config = { port: process.env.PORT || 3000, databaseUrl: process.env.DATABASE_URL || '', + docsUrl: process.env.DOCS_URL || '', jwt: { secret: process.env.JWT_SECRET || '', }, From 9cc4f811442734b67c0cb67b747fcd36a0bfbcdb Mon Sep 17 00:00:00 2001 From: salmanabdurrahman Date: Wed, 15 Oct 2025 20:29:21 +0700 Subject: [PATCH 3/4] feat(community): implement like/unlike functionality for community posts - add community post like table to prisma schema - create community post like table migration - implement toggleLikeOnPost service function to handle like/unlike logic - update addLikeHandler to use toggleLikeOnPost service - return like status and count in the response - add user id validation to addLikeHandler --- .../migration.sql | 16 +++++ prisma/schema.prisma | 12 ++++ src/api/community/community.controller.ts | 18 +++++- src/api/community/community.service.ts | 62 ++++++++++++++++--- 4 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 prisma/migrations/20251015124514_add_community_post_like_tables/migration.sql diff --git a/prisma/migrations/20251015124514_add_community_post_like_tables/migration.sql b/prisma/migrations/20251015124514_add_community_post_like_tables/migration.sql new file mode 100644 index 0000000..d6cc175 --- /dev/null +++ b/prisma/migrations/20251015124514_add_community_post_like_tables/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "public"."CommunityPostLike" ( + "userId" TEXT NOT NULL, + "postId" TEXT NOT NULL, + + CONSTRAINT "CommunityPostLike_pkey" PRIMARY KEY ("userId","postId") +); + +-- CreateIndex +CREATE INDEX "CommunityPostLike_postId_idx" ON "public"."CommunityPostLike"("postId"); + +-- AddForeignKey +ALTER TABLE "public"."CommunityPostLike" ADD CONSTRAINT "CommunityPostLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."CommunityPostLike" ADD CONSTRAINT "CommunityPostLike_postId_fkey" FOREIGN KEY ("postId") REFERENCES "public"."CommunityPost"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 56aa2d3..c86c827 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -30,6 +30,7 @@ model User { posts CommunityPost[] comments CommunityComment[] profile UserProfile? + postLikes CommunityPostLike[] } model Streak { @@ -81,10 +82,21 @@ model CommunityPost { userId String user User @relation(fields: [userId], references: [id]) comments CommunityComment[] + postLikes CommunityPostLike[] @@index([userId]) } +model CommunityPostLike { + userId String + postId String + user User @relation(fields: [userId], references: [id]) + post CommunityPost @relation(fields: [postId], references: [id]) + + @@id([userId, postId]) + @@index([postId]) +} + model CommunityComment { id String @id @default(uuid()) content String diff --git a/src/api/community/community.controller.ts b/src/api/community/community.controller.ts index b75292b..b4322de 100644 --- a/src/api/community/community.controller.ts +++ b/src/api/community/community.controller.ts @@ -1,9 +1,9 @@ import type { Request, Response } from 'express'; import { - addLikeToPost, createComment, createPost, findAllPosts, + toggleLikeOnPost, type PostCategory, } from './community.service.js'; import { asyncHandler } from '../../handler/async.handler.js'; @@ -54,11 +54,23 @@ export const createCommentHandler = asyncHandler(async (req: Request, res: Respo }); export const addLikeHandler = asyncHandler(async (req: Request, res: Response) => { + const userId = req.user?.id; const { postId } = req.params; + + if (!userId) { + return errorResponse( + res, + 401, + 'Tidak diizinkan', + 'ID pengguna tidak ditemukan dalam permintaan' + ); + } if (!postId) { return errorResponse(res, 400, 'Permintaan Tidak Valid', 'ID postingan diperlukan'); } - const post = await addLikeToPost(postId); - return successResponse(res, 200, 'Postingan berhasil disukai', { likeCount: post.likeCount }); + const post = await toggleLikeOnPost(userId, postId); + const message = post.isLiked ? 'Postingan berhasil disukai' : 'Suka pada postingan dibatalkan'; + + return successResponse(res, 200, message, post); }); diff --git a/src/api/community/community.service.ts b/src/api/community/community.service.ts index 8ccd70a..7c4f8b1 100644 --- a/src/api/community/community.service.ts +++ b/src/api/community/community.service.ts @@ -109,17 +109,63 @@ export async function createComment(userId: string, postId: string, content: str }); } -export async function addLikeToPost(postId: string) { - const updatedPost = await prisma.communityPost.update({ +export async function toggleLikeOnPost(userId: string, postId: string) { + const existingLike = await prisma.communityPostLike.findUnique({ where: { - id: postId, - }, - data: { - likeCount: { - increment: 1, + userId_postId: { + userId, + postId, }, }, }); - return updatedPost; + if (existingLike) { + const updatedPost = await prisma.$transaction([ + prisma.communityPostLike.delete({ + where: { + userId_postId: { + userId, + postId, + }, + }, + }), + prisma.communityPost.update({ + where: { + id: postId, + }, + data: { + likeCount: { + decrement: 1, + }, + }, + }), + ]); + return { + likedCount: updatedPost[1].likeCount, + isLiked: false, + }; + } else { + const updatedPost = await prisma.$transaction([ + prisma.communityPostLike.create({ + data: { + userId, + postId, + }, + }), + prisma.communityPost.update({ + where: { + id: postId, + }, + data: { + likeCount: { + increment: 1, + }, + }, + }), + ]); + return { + likedCount: updatedPost[1].likeCount, + isLiked: true, + }; + } } From bedc228bd69e8da16339dc2aef5478d2d6226b29 Mon Sep 17 00:00:00 2001 From: salmanabdurrahman Date: Wed, 15 Oct 2025 20:30:21 +0700 Subject: [PATCH 4/4] feat(auth, form, middleware): protect daily content route, update validation, and clean up error paths - Protect daily content route with requireAuth middleware - Allow shorter 'userWhy' in updateUserSettingsSchema (min length 3) - Remove prefix from zod error path for cleaner error messages --- src/api/content/content.routes.ts | 3 ++- src/api/users/user.validation.ts | 2 +- src/middleware/validate.middleware.ts | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/api/content/content.routes.ts b/src/api/content/content.routes.ts index 1794a3e..e0f95d9 100644 --- a/src/api/content/content.routes.ts +++ b/src/api/content/content.routes.ts @@ -1,8 +1,9 @@ import { Router } from 'express'; import { getDailyContentHandler } from './content.controller.js'; +import { requireAuth } from '../../middleware/auth.middleware.js'; const router = Router(); -router.get('/daily', getDailyContentHandler); +router.get('/daily', requireAuth, getDailyContentHandler); export default router; diff --git a/src/api/users/user.validation.ts b/src/api/users/user.validation.ts index 96d45f8..ef8e7fd 100644 --- a/src/api/users/user.validation.ts +++ b/src/api/users/user.validation.ts @@ -4,7 +4,7 @@ export const updateUserSettingsSchema = z.object({ body: z .object({ nickname: z.string().min(3, 'Nama panggilan harus terdiri dari minimal 3 karakter').trim(), - userWhy: z.string().min(10, 'Alasan Anda harus terdiri dari minimal 10 karakter').trim(), + userWhy: z.string().min(3, 'Alasan Anda harus terdiri dari minimal 3 karakter').trim(), checkinTime: z .string() .regex(/^([01]\d|2[0-3]):([0-5]\d)$/, { diff --git a/src/middleware/validate.middleware.ts b/src/middleware/validate.middleware.ts index 1a35bed..d4bd1de 100644 --- a/src/middleware/validate.middleware.ts +++ b/src/middleware/validate.middleware.ts @@ -15,7 +15,8 @@ export const validate = if (error instanceof ZodError) { const errorDetails = error.issues.reduce( (acc, curr) => { - const path = curr.path.join('.') || 'unknown'; + let path = curr.path.join('.') || 'unknown'; + path = path.replace(/^(body|query|params)\./, ''); acc[path] = curr.message; return acc; },