diff --git a/apps/api/package.json b/apps/api/package.json index 3a989ea..93b3734 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -41,6 +41,7 @@ "ioredis": "^5.6.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "multer": "^2.0.1", "twilio": "^5.4.5", "zod": "^3.25.6" }, @@ -54,6 +55,7 @@ "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.9", "@types/morgan": "^1.9.9", + "@types/multer": "^1.4.13", "@types/node": "^22.13.5", "@types/supertest": "^6.0.2", "eslint": "^9.20.0", diff --git a/apps/api/src/modules/user/user.route.ts b/apps/api/src/modules/user/user.route.ts index c2d7ba1..8ee12a6 100644 --- a/apps/api/src/modules/user/user.route.ts +++ b/apps/api/src/modules/user/user.route.ts @@ -6,8 +6,13 @@ import { createTypiRouter, } from "@repo/typiserver"; import { db } from "@repo/database"; -import { users } from "@repo/database/schema"; +import { + refreshTokens, + userPrivacySettings, + users, +} from "@repo/database/schema"; import authMiddleware from "@repo/api/middlewares/auth"; +import { createUpdateSchema } from "drizzle-zod"; const userRouter = createTypiRouter({ "/": createTypiRoute({ @@ -27,9 +32,11 @@ const userRouter = createTypiRouter({ patch: createTypiRouteHandler({ input: { body: z.object({ - name: z.string().optional(), - description: z.string().optional(), - picture: z.string().optional(), + user: z.object({ + name: z.string().optional(), + description: z.string().optional(), + picture: z.instanceof(File).optional(), + }), }), }, middlewares: [authMiddleware], @@ -37,8 +44,9 @@ const userRouter = createTypiRouter({ const user = await db .update(users) .set({ + name: ctx.input.body.user.name, + description: ctx.input.body.user.description, updatedAt: new Date(), - ...ctx.input.body, }) .where(eq(users.id, ctx.data.userId)) .returning(); @@ -50,6 +58,71 @@ const userRouter = createTypiRouter({ }); }, }), + delete: createTypiRouteHandler({ + middlewares: [authMiddleware], + handler: async (ctx) => { + const deleted = await db + .delete(users) + .where(eq(users.id, ctx.data.userId)) + .returning(); + + if (!deleted.length) return ctx.error("NOT_FOUND", "User not found."); + + await db + .delete(refreshTokens) + .where(eq(refreshTokens.userId, ctx.data.userId)); + + ctx.response.clearCookie(process.env.JWT_ACCESS_TOKEN_COOKIE_KEY); + ctx.response.clearCookie(process.env.JWT_REFRESH_TOKEN_COOKIE_KEY); + + return ctx.success({ + message: "User deleted successfully.", + }); + }, + }), + }), + "/privacy": createTypiRoute({ + get: createTypiRouteHandler({ + middlewares: [authMiddleware], + handler: async (ctx) => { + const privacySettings = await db.query.userPrivacySettings.findFirst({ + where: and(eq(users.id, ctx.data.userId)), + }); + if (!privacySettings) return ctx.error("NOT_FOUND", "User not found."); + + return ctx.success({ + privacySettings, + }); + }, + }), + patch: createTypiRouteHandler({ + input: { + body: z.object({ + privacySettings: createUpdateSchema(userPrivacySettings, { + createdAt: z.undefined(), + updatedAt: z.undefined(), + }), + }), + }, + middlewares: [authMiddleware], + handler: async (ctx) => { + const updatedPrivacySettings = await db + .update(userPrivacySettings) + .set({ + updatedAt: new Date(), + ...ctx.input.body.privacySettings, + }) + .where(eq(userPrivacySettings.userId, ctx.data.userId)) + .returning(); + + if (!updatedPrivacySettings.length) + return ctx.error("NOT_FOUND", "User not found."); + + return ctx.success({ + privacySettings: updatedPrivacySettings[0], + }); + }, + }), }), }); diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index c80aca5..437005f 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -1,31 +1,26 @@ import express, { type Express } from "express"; -import morgan from "morgan"; +import multer from "multer"; import cors from "cors"; import cookieParser from "cookie-parser"; import rootRouter from "@repo/api/modules"; import errorHandler from "@repo/api/middlewares/error"; -import rateLimitMiddleware from "@repo/api/middlewares/rate-limit"; export const createServer = (): Express => { const app = express(); app .disable("x-powered-by") - .use( - morgan( - `:remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length]`, - { immediate: true } - ) - ) .use(express.urlencoded({ extended: true })) .use(express.json()) + .use( + multer({ + storage: multer.memoryStorage(), // Store files in memory + limits: { + fileSize: 10 * 1024 * 1024, // 10MB limit + }, + }).any() + ) .use(cookieParser()) .use(cors({ origin: "http://localhost:3002", credentials: true })) - // .use( - // rateLimitMiddleware({ - // timeSpan: process.env.WINDOW_SIZE_IN_MINUTES, - // limit: process.env.MAX_NUMBER_OF_REQUESTS_PER_WINDOW_SIZE, - // }) - // ) .use("/api/v1", rootRouter.router) .all("/*splat", () => { throw new Error("You look a little lost.");