import type { Route } from "./+types/settings"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { ArrowLeft, Save, Trash2, UserPlus, UserMinus, Search, X } from "lucide-react"; import { Link, Form, redirect } from "react-router"; import { db } from "@lib/db"; import { projects, users, projectPermissions, type Project, type User, type ProjectPermission } from "@lib/db/schema"; import { getCurrentUser } from "@lib/auth-utils"; import Layout from "@/components/layout"; import { eq, and, like } from "drizzle-orm"; import { canUserEditProject } from "@lib/auth"; import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Checkbox } from "@/components/ui/checkbox"; import { UserSearchModal } from "@/components/project/UserSearch"; export function meta({}: Route.MetaArgs) { return [ { title: "Project Settings" }, { name: "description", content: "Manage project settings and permissions" } ]; } export async function loader({ request, params }: Route.LoaderArgs) { const user = await getCurrentUser(request); if (!user) { throw new Response("Unauthorized", { status: 401 }); } const projectId = params.id; if (!projectId) { throw new Response("Project ID required", { status: 400 }); } // Get project details const project = await db.select().from(projects).where(eq(projects.id, projectId)).get(); if (!project) { throw new Response("Project not found", { status: 404 }); } // Check if user can edit this project const canEdit = await canUserEditProject(user.id, projectId); if (!canEdit) { throw new Response("Forbidden", { status: 403 }); } // Get all users for the user management section const allUsers = await db.select().from(users).orderBy(users.username); // Get current project permissions const currentPermissions = await db .select() .from(projectPermissions) .where(eq(projectPermissions.projectId, projectId)); const isOwner = await db .select() .from(projects) .where(and(eq(projects.id, projectId), eq(projects.ownerId, user.id))) .get(); return { project, allUsers, currentPermissions, currentUser: user, isOwner }; } export async function action({ request, params }: Route.ActionArgs) { const user = await getCurrentUser(request); const projectId = params.id; if (!projectId) { throw new Response("Project ID required", { status: 400 }); } const project = await db.select().from(projects).where(eq(projects.id, projectId)).get(); if (!user) { throw new Response("Unauthorized", { status: 401 }); } const formData = await request.formData(); const intent = formData.get("intent") as string; // Check if user can edit this project const canEdit = await canUserEditProject(user.id, projectId); if (!canEdit) { throw new Response("Forbidden", { status: 403 }); } const isOwner = await db .select() .from(projects) .where(and(eq(projects.id, projectId), eq(projects.ownerId, user.id))) .get(); if (intent === "updateProject") { const name = formData.get("name") as string; const description = formData.get("description") as string; const isPublic = isOwner ? formData.get("isPublic") === "on" : project?.isPublic; if (!name) { return { error: "Project name is required" }; } await db .update(projects) .set({ name, description, isPublic, updatedAt: new Date() }) .where(eq(projects.id, projectId)); return redirect(`/project/${projectId}`); } // Danger zone (below): only project owner have access if (!project || project.ownerId !== user.id) { throw new Response("You do not have permission to edit this project", { status: 403 }); } if (intent === "deleteProject") { await db.delete(projects).where(eq(projects.id, projectId)); return redirect(`/projects`); } if (intent === "addUser") { const userId = formData.get("userId") as string; const canEditPermission = formData.get("canEdit") === "on"; if (!userId) { return { error: "User ID is required" }; } // Check if permission already exists const existingPermission = await db .select() .from(projectPermissions) .where( and( eq(projectPermissions.projectId, projectId), eq(projectPermissions.userId, userId) ) ) .get(); if (existingPermission) { return { error: "User already has permission for this project" }; } await db.insert(projectPermissions).values({ id: crypto.randomUUID(), projectId, userId, canEdit: canEditPermission, createdAt: new Date() }); return redirect(`/project/${projectId}`); } if (intent === "removeUser") { const userId = formData.get("userId") as string; if (!userId) { return { error: "User ID is required" }; } // Don't allow removing the project owner const project = await db.select().from(projects).where(eq(projects.id, projectId)).get(); if (project && project.ownerId === userId) { return { error: "Cannot remove project owner" }; } await db .delete(projectPermissions) .where( and( eq(projectPermissions.projectId, projectId), eq(projectPermissions.userId, userId) ) ); return redirect(`/project/${projectId}`); } if (intent === "updatePermission") { const userId = formData.get("userId") as string; const canEdit = formData.get("canEdit") === "on"; if (!userId) { return { error: "User ID is required" }; } // Don't allow changing the project owner's permissions const project = await db.select().from(projects).where(eq(projects.id, projectId)).get(); if (project && project.ownerId === userId) { return { error: "Cannot change project owner's permissions" }; } await db .update(projectPermissions) .set({ canEdit }) .where( and( eq(projectPermissions.projectId, projectId), eq(projectPermissions.userId, userId) ) ); return redirect(`/project/${projectId}`); } return { error: "Unknown action" }; } interface UsersManagementProps { project: Project; availableUsers: User[]; currentPermissions: ProjectPermission[]; allUsers: User[]; } export function UsersManagement({ project, availableUsers, currentPermissions, allUsers }: UsersManagementProps) { return ( User Permissions Manage who can view and edit this project {/* Add User Form */}

Add User

{/* Current Users Table */}

Current Users

Username Role Permissions Actions {/* Project Owner */} {allUsers.find((u) => u.id === project.ownerId)?.username} Owner Owner Full Access - {/* Users with permissions */} {currentPermissions.map((permission) => { const user = allUsers.find((u) => u.id === permission.userId); if (!user) return null; return ( {user.username} Collaborator
{ const canEdit = e; const formData = new FormData(); formData.append( "intent", "updatePermission" ); formData.append("userId", user.id); formData.append( "canEdit", canEdit ? "on" : "" ); fetch( `/project/${project.id}/settings`, { method: "POST", body: formData } ); }} />
); })}
); } export default function ProjectSettings({ loaderData }: Route.ComponentProps) { const { project, allUsers, currentPermissions, currentUser } = loaderData; // Create a map of user permissions for easy lookup const userPermissions = new Map(); currentPermissions.forEach((permission) => { userPermissions.set(permission.userId, permission); }); // Get users who don't have permissions yet const availableUsers = allUsers.filter( (user) => !userPermissions.has(user.id) && user.id !== project.ownerId ); return ( {/* Header */}

Project Settings

Manage project details and user permissions

{/* Project Details */} Project Details Update project name, description, and visibility