Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions front/src/app/api/admin/check-permission/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
12 changes: 8 additions & 4 deletions front/src/app/room/[roomId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ export default async function RoomPage({
}

return (
<ClosedEventTemplate
userIcons={userIcons}
imgUrl={event.imageUrl}
/>
<SessionProvider>
<ClosedEventTemplate
userIcons={userIcons}
imgUrl={event.imageUrl}
/>
</SessionProvider>
)
}

Expand All @@ -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 ?? "";
Expand All @@ -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);
}
}
Expand Down
63 changes: 63 additions & 0 deletions front/src/components/organisms/room/DeleteConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>ユーザーを削除しますか?</DialogTitle>
<DialogDescription>
「{userName}」をルームから削除します。この操作は取り消せません。
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={isDeleting}>
キャンセル
</Button>
<Button
variant="destructive"
onClick={handleConfirm}
disabled={isDeleting}
>
{isDeleting ? "削除中..." : "削除"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
102 changes: 81 additions & 21 deletions front/src/components/organisms/room/NonDraggableIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"use client";

import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import {
Avatar,
AvatarFallback,
Expand All @@ -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 (
<div
className="absolute size-12"
style={{transform: `translate(${icon.position.x}px, ${icon.position.y}px)`}}
>
<UserIconTooltip user={icon.user}>
<Avatar
className="size-12"
onClick={(e) => e.preventDefault()}
onPointerDown={(e) => e.preventDefault()}
>
<AvatarImage
src={`${icon.user.image}`}
draggable="false"
referrerPolicy="no-referrer"
/>
<AvatarFallback><User /></AvatarFallback>
</Avatar>
</UserIconTooltip>
</div>
<>
<div
className="absolute size-12"
style={{transform: `translate(${icon.position.x}px, ${icon.position.y}px)`}}
>
<UserIconTooltip user={icon.user}>
<Avatar
className={`size-12 ${canDelete ? 'cursor-pointer hover:opacity-80' : ''}`}
onClick={handleClick}
onPointerDown={(e) => e.preventDefault()}
>
<AvatarImage
src={`${icon.user.image}`}
draggable="false"
referrerPolicy="no-referrer"
/>
<AvatarFallback><User /></AvatarFallback>
</Avatar>
</UserIconTooltip>
</div>

{canDelete && (
<DeleteConfirmDialog
isOpen={showDeleteDialog}
onClose={() => setShowDeleteDialog(false)}
onConfirm={handleDeleteConfirm}
userName={icon.user.name || "Unknown User"}
/>
)}
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export default function ActiveEventTemplate({
{imgUrl && <VenueImage imgUrl={imgUrl}/>}
{userIcons.map((icon, i) => {
if (icon.user.id !== userId) {
return <NonDraggableIcon key={i} icon={icon}/>
return <NonDraggableIcon key={i} icon={icon} roomId={roomId}/>
} else {
return <DraggableIcon key={i} socket={socket} icon={icon} scale={scale}/>
}
Expand Down
21 changes: 21 additions & 0 deletions front/src/lib/api/checkAdminPermission.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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;
}
}
15 changes: 15 additions & 0 deletions front/src/lib/api/deleteUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { PARTYKIT_URL } from "@/app/env";

export async function deleteUserFromRoom(roomId: string, userId: string): Promise<void> {
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}`);
}
}
2 changes: 1 addition & 1 deletion front/src/lib/db/finished_event_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
20 changes: 16 additions & 4 deletions partykit/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand All @@ -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") {
Expand All @@ -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) {
Expand Down