diff --git a/front/src/app/api/admin/check-permission/route.ts b/front/src/app/api/admin/check-permission/route.ts
new file mode 100644
index 0000000..d9c5550
--- /dev/null
+++ b/front/src/app/api/admin/check-permission/route.ts
@@ -0,0 +1,26 @@
+import { NextRequest, NextResponse } from "next/server";
+import { auth } from "@/lib/auth/auth";
+import { checkAdminPermission } from "@/lib/api/checkAdminPermission";
+
+export async function GET(request: NextRequest) {
+ try {
+ const session = await auth();
+ if (!session?.user?.email) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { searchParams } = new URL(request.url);
+ const roomId = searchParams.get("roomId");
+
+ if (!roomId) {
+ return NextResponse.json({ error: "roomId is required" }, { status: 400 });
+ }
+
+ const isAdmin = await checkAdminPermission(session.user.email, roomId);
+
+ return NextResponse.json({ isAdmin });
+ } catch (error) {
+ console.error("Error checking admin permission:", error);
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/front/src/app/room/[roomId]/page.tsx b/front/src/app/room/[roomId]/page.tsx
index ca1ecc4..e95f967 100644
--- a/front/src/app/room/[roomId]/page.tsx
+++ b/front/src/app/room/[roomId]/page.tsx
@@ -45,10 +45,12 @@ export default async function RoomPage({
}
return (
-
+
+
+
)
}
@@ -60,6 +62,7 @@ export default async function RoomPage({
const res = await selectUserByEmail(user.email);
if (res) {
+ user.id = res.id.toString();
user.name = res.name;
user.bio = res.bio ?? "";
user.interests = res.interests ?? "";
@@ -72,6 +75,7 @@ export default async function RoomPage({
user.name = "New User";
const userId = await insertNewUser(user.email);
+ user.id = userId.toString();
await insertMemberAssignment(userId, event.userGroupId);
}
}
diff --git a/front/src/components/organisms/room/DeleteConfirmDialog.tsx b/front/src/components/organisms/room/DeleteConfirmDialog.tsx
new file mode 100644
index 0000000..02c4961
--- /dev/null
+++ b/front/src/components/organisms/room/DeleteConfirmDialog.tsx
@@ -0,0 +1,63 @@
+"use client";
+
+import { useState } from "react";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/atoms/shadcn/dialog";
+import { Button } from "@/components/atoms/shadcn/button";
+
+interface DeleteConfirmDialogProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ userName: string;
+}
+
+export default function DeleteConfirmDialog({
+ isOpen,
+ onClose,
+ onConfirm,
+ userName,
+}: DeleteConfirmDialogProps) {
+ const [isDeleting, setIsDeleting] = useState(false);
+
+ const handleConfirm = async () => {
+ setIsDeleting(true);
+ try {
+ await onConfirm();
+ } finally {
+ setIsDeleting(false);
+ onClose();
+ }
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/front/src/components/organisms/room/NonDraggableIcon.tsx b/front/src/components/organisms/room/NonDraggableIcon.tsx
index 9c36279..f1cad07 100644
--- a/front/src/components/organisms/room/NonDraggableIcon.tsx
+++ b/front/src/components/organisms/room/NonDraggableIcon.tsx
@@ -1,3 +1,7 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useSession } from "next-auth/react";
import {
Avatar,
AvatarFallback,
@@ -6,32 +10,88 @@ import {
import { User } from "lucide-react";
import { UserIcon } from "@/types/room/shared";
import UserIconTooltip from "./UserIconTooltip";
+import DeleteConfirmDialog from "./DeleteConfirmDialog";
+import { deleteUserFromRoom } from "@/lib/api/deleteUser";
export default function NonDraggableIcon({
- icon
+ icon,
+ roomId
} : {
- icon: UserIcon
+ icon: UserIcon,
+ roomId?: string
}) {
+ const { data: session } = useSession();
+ const [showDeleteDialog, setShowDeleteDialog] = useState(false);
+ const [isAdmin, setIsAdmin] = useState(false);
+
+ // roomIdがない場合(終了したイベント)は削除機能を無効化
+ const canDelete = roomId && isAdmin;
+
+ useEffect(() => {
+ const checkAdmin = async () => {
+ if (session?.user?.email && roomId) {
+ try {
+ const response = await fetch(`/api/admin/check-permission?roomId=${roomId}`);
+ if (response.ok) {
+ const data = await response.json();
+ setIsAdmin(data.isAdmin);
+ }
+ } catch (error) {
+ console.error("Failed to check admin permission:", error);
+ setIsAdmin(false);
+ }
+ }
+ };
+
+ checkAdmin();
+ }, [session?.user?.email, roomId]);
+
+ const handleClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ if (canDelete) {
+ setShowDeleteDialog(true);
+ }
+ };
+
+ const handleDeleteConfirm = async () => {
+ if (!roomId) return;
+ try {
+ await deleteUserFromRoom(roomId, icon.user.id);
+ } catch (error) {
+ console.error("Failed to delete user:", error);
+ }
+ };
return (
-
-
- e.preventDefault()}
- onPointerDown={(e) => e.preventDefault()}
- >
-
-
-
-
-
+ <>
+
+
+ e.preventDefault()}
+ >
+
+
+
+
+
+
+ {canDelete && (
+ setShowDeleteDialog(false)}
+ onConfirm={handleDeleteConfirm}
+ userName={icon.user.name || "Unknown User"}
+ />
+ )}
+ >
)
}
diff --git a/front/src/components/templates/room/ActiveEventTemplate.tsx b/front/src/components/templates/room/ActiveEventTemplate.tsx
index 15f0c77..d44ba2e 100644
--- a/front/src/components/templates/room/ActiveEventTemplate.tsx
+++ b/front/src/components/templates/room/ActiveEventTemplate.tsx
@@ -82,7 +82,7 @@ export default function ActiveEventTemplate({
{imgUrl && }
{userIcons.map((icon, i) => {
if (icon.user.id !== userId) {
- return
+ return
} else {
return
}
diff --git a/front/src/lib/api/checkAdminPermission.ts b/front/src/lib/api/checkAdminPermission.ts
new file mode 100644
index 0000000..0bb08c0
--- /dev/null
+++ b/front/src/lib/api/checkAdminPermission.ts
@@ -0,0 +1,21 @@
+import { selectEventById } from "@/lib/db/event";
+import { isUserAdminOfGroup } from "@/lib/db/user_group_assignment";
+import { selectUserByEmail } from "@/lib/db/user";
+
+export async function checkAdminPermission(userEmail: string, roomId: string): Promise {
+ try {
+ const eventId = Number(roomId);
+ if (isNaN(eventId)) return false;
+
+ const event = await selectEventById(eventId);
+ if (!event) return false;
+
+ const user = await selectUserByEmail(userEmail);
+ if (!user) return false;
+
+ return await isUserAdminOfGroup(user.id, event.userGroupId);
+ } catch (error) {
+ console.error("Failed to check admin permission:", error);
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/front/src/lib/api/deleteUser.ts b/front/src/lib/api/deleteUser.ts
new file mode 100644
index 0000000..797e8bd
--- /dev/null
+++ b/front/src/lib/api/deleteUser.ts
@@ -0,0 +1,15 @@
+import { PARTYKIT_URL } from "@/app/env";
+
+export async function deleteUserFromRoom(roomId: string, userId: string): Promise {
+ const response = await fetch(`${PARTYKIT_URL}/parties/main/${roomId}`, {
+ method: "DELETE",
+ body: JSON.stringify(userId),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to delete user: ${response.status} ${response.statusText}`);
+ }
+}
\ No newline at end of file
diff --git a/front/src/lib/db/finished_event_state.ts b/front/src/lib/db/finished_event_state.ts
index 41be4c3..da82e6e 100644
--- a/front/src/lib/db/finished_event_state.ts
+++ b/front/src/lib/db/finished_event_state.ts
@@ -14,7 +14,7 @@ export async function selectFinishedEventState(eventId: number) {
.select()
.from(finishedEventState)
.where(eq(finishedEventState.eventId, eventId));
- return res[0].userIcons as UserIcon[] ?? null;
+ return res[0]?.userIcons as UserIcon[] ?? null;
}
/**
diff --git a/partykit/src/server.ts b/partykit/src/server.ts
index 7ac7d5b..c0de090 100644
--- a/partykit/src/server.ts
+++ b/partykit/src/server.ts
@@ -58,10 +58,22 @@ export default class Server implements Party.Server {
}
async onRequest(request: Party.Request) {
+ // CORS headers
+ const corsHeaders = {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type",
+ };
+
+ // Handle preflight requests
+ if (request.method === "OPTIONS") {
+ return new Response(null, { status: 200, headers: corsHeaders });
+ }
+
const userIcons = await this.ensureLoadUserIcons();
if (request.method === "GET") {
- return new Response(JSON.stringify(userIcons));
+ return new Response(JSON.stringify(userIcons), { headers: corsHeaders });
}
if (request.method === "POST") {
@@ -76,7 +88,7 @@ export default class Server implements Party.Server {
await this.room.storage.put("userIcons", this.userIcons);
}
- return new Response(JSON.stringify(user.id));
+ return new Response(JSON.stringify(user.id), { headers: corsHeaders });
}
if (request.method === "DELETE") {
@@ -88,10 +100,10 @@ export default class Server implements Party.Server {
});
await this.room.storage.put("userIcons", this.userIcons);
- return new Response(null, { status: 204 });
+ return new Response(null, { status: 204, headers: corsHeaders });
}
- return new Response("Not Found", { status: 404 });
+ return new Response("Not Found", { status: 404, headers: corsHeaders });
}
async onConnect(conn: Party.Connection, _ctx: Party.ConnectionContext) {