diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index 1d75439..946eca9 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -263,6 +263,57 @@ export default new Elysia({ prefix: "/auth" }) }), }) + .delete("/user/:id", async ({ params, jwt, cookie: { mizuki }, status }) => { + const rawToken = mizuki.value; + if (typeof rawToken !== "string" || rawToken.length === 0) { + return status(401, "Unauthorized"); + } + + const payload = await jwt.verify(rawToken); + if (!payload || typeof payload !== "object" || !("id" in payload) || typeof payload.id !== "string") { + return status(401, "Unauthorized"); + } + + const requester = await User.findOne({ userId: payload.id }); + if (!requester || requester.role !== "admin") { + return status(403, "Forbidden"); + } + + const target = await User.findOne({ userId: params.id }); + if (!target) { + return status(404, "User not found"); + } + + if (hardcodedOwners.has(target.discordId)) { + return status(400, "Hardcoded owners cannot be deleted"); + } + + await User.deleteOne({ userId: params.id }); + + await createAuditLog({ + actor: { + userId: requester.userId, + discordId: requester.discordId, + username: requester.username, + role: requester.role, + }, + action: "auth.user.delete", + targetType: "user", + targetId: params.id, + summary: `${requester.username} deleted user ${target.username}`, + detail: { + userId: target.userId, + discordId: target.discordId, + }, + }); + + return { ok: true }; + }, { + params: t.Object({ + id: t.String(), + }) + }) + .get("/users", async ({ jwt, cookie: { mizuki }, status }) => { const rawToken = mizuki.value; if (typeof rawToken !== "string" || rawToken.length === 0) { diff --git a/apps/backend/src/routes/post.ts b/apps/backend/src/routes/post.ts index 20168b0..940b50e 100644 --- a/apps/backend/src/routes/post.ts +++ b/apps/backend/src/routes/post.ts @@ -217,7 +217,17 @@ export default new Elysia({ prefix: "/post" }) }) }) - .get("/list", async ({ query }) => { + .get("/list", async ({ query, jwt, cookie: { mizuki }, status }) => { + const rawToken = mizuki.value; + if (typeof rawToken !== "string" || rawToken.length === 0) { + return status(401, "Unauthorized"); + } + + const payload = await jwt.verify(rawToken); + if (!payload || typeof payload !== "object" || !("id" in payload) || typeof payload.id !== "string") { + return status(401, "Unauthorized"); + } + const page = query.page; const pageSize = query.size || 10; const filterTags = normalizeQueryTags(query.tags); @@ -260,22 +270,7 @@ export default new Elysia({ prefix: "/post" }) }), }) - .get("/detail/:id", async ({ params, status, jwt, cookie: { mizuki } }) => { - const rawToken = mizuki.value; - if (typeof rawToken !== "string" || rawToken.length === 0) { - return status(401, "Unauthorized"); - } - - const payload = await jwt.verify(rawToken); - if (!payload || typeof payload !== "object" || !("id" in payload) || typeof payload.id !== "string") { - return status(401, "Unauthorized"); - } - - const user = await User.findOne({ userId: payload.id }); - if (!user) { - return status(401, "Unauthorized"); - } - + .get("/detail/:id", async ({ params, status }) => { const post = await MediaUpload.findById(params.id); if (!post) { return status(404, "포스트를 찾을 수 없습니다."); diff --git a/apps/frontend/src/app/dashboard/page.tsx b/apps/frontend/src/app/dashboard/page.tsx index 87b81a8..41b0839 100644 --- a/apps/frontend/src/app/dashboard/page.tsx +++ b/apps/frontend/src/app/dashboard/page.tsx @@ -416,16 +416,28 @@ export default function DashboardPage() { - +
+ + +
))} diff --git a/apps/frontend/src/app/globals.css b/apps/frontend/src/app/globals.css index a2dfe34..f3efdf0 100644 --- a/apps/frontend/src/app/globals.css +++ b/apps/frontend/src/app/globals.css @@ -102,6 +102,10 @@ transition-timing-function: var(--ease-out-expo); } +html, body { + overscroll-behavior-y: none; +} + @media (hover: hover) { .image-scale:hover { --tw-scale-x: 105%;