Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
암냥 2026-05-03 22:17:19 +09:00
commit 3253e91d95
4 changed files with 89 additions and 27 deletions

View file

@ -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) {

View file

@ -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, "포스트를 찾을 수 없습니다.");

View file

@ -416,16 +416,28 @@ export default function DashboardPage() {
</select>
</td>
<td className="px-3 py-2">
<button
type="button"
onClick={() => {
void updateRole(user);
}}
disabled={savingUserId === user.id || (pendingRole[user.id] ?? user.role) === user.role}
className="border border-border px-3 py-1 disabled:opacity-50"
>
{savingUserId === user.id ? "저장 중..." : "저장"}
</button>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
void updateRole(user);
}}
disabled={savingUserId === user.id || (pendingRole[user.id] ?? user.role) === user.role}
className="border border-border px-3 py-1 disabled:opacity-50"
>
{savingUserId === user.id ? "저장 중..." : "저장"}
</button>
<button
type="button"
onClick={() => {
void deleteUser(user);
}}
disabled={savingUserId === user.id}
className="border border-red-300 bg-red-50 px-3 py-1 text-xs text-red-700 disabled:opacity-50"
>
</button>
</div>
</td>
</tr>
))}

View file

@ -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%;