import Link from "next/link"; import { notFound } from "next/navigation"; import { and, eq, inArray, isNull, or } from "drizzle-orm"; import { auth } from "@/auth"; import { db } from "@/lib/db/client"; import { snippets, projects, users } from "@/lib/db/schema"; import { updateSnippetAction, deleteSnippetAction } from "@/lib/snippet-actions"; import { getSnippet, type SnippetWithProjectKey } from "@/lib/snippets"; import { getProjectAccess, getUserGroupNames, readableProjectIds } from "@/lib/access"; import { Container, PageHeader } from "@/app/_components/ui/container"; import { Card, CardBody, CardHeader } from "@/app/_components/ui/card"; import { Input, Textarea, Label } from "@/app/_components/ui/input"; import { Button } from "@/app/_components/ui/button"; import { Badge } from "@/app/_components/ui/badge"; export const dynamic = "force-dynamic"; interface SiblingHit { scope: "project" | "user"; projectKey: string | null; } /** * When a snippet name exists in more than one scope (e.g. a user-scope * default plus one or more project-scope variants), we need to either * disambiguate by query string or, if no hint is given, show a picker. * * Visibility widening: with sharing, the user may also see project- * scope snippets under shared projects. Match rows that the viewer can * read (own user-scope rows, or project-scope rows in an accessible * project). */ async function findAllMatches( userId: string, groupNames: string[], name: string, ): Promise { const accessibleProjectIds = await readableProjectIds(userId, groupNames); const visibility = accessibleProjectIds.length > 0 ? or( and(eq(snippets.userId, userId), isNull(snippets.projectId)), inArray(snippets.projectId, accessibleProjectIds), ) : and(eq(snippets.userId, userId), isNull(snippets.projectId)); const rows = await db .select({ scope: snippets.scope, projectKey: projects.key, }) .from(snippets) .leftJoin(projects, eq(snippets.projectId, projects.id)) .where(and(eq(snippets.name, name), isNull(snippets.deletedAt), visibility!)); return rows as SiblingHit[]; } export default async function SnippetDetailPage({ params, searchParams, }: { params: Promise<{ name: string }>; searchParams: Promise<{ scope?: string; project?: string; edit?: string }>; }) { const session = await auth(); const userId = session!.user.id; const groupNames = await getUserGroupNames(userId); const { name: rawName } = await params; const name = decodeURIComponent(rawName); const sp = await searchParams; const scope: "project" | "user" | undefined = sp.scope === "user" || sp.scope === "project" ? sp.scope : undefined; const project = sp.project?.trim() || undefined; const wantsEdit = sp.edit === "1"; const siblings = await findAllMatches(userId, groupNames, name); if (siblings.length === 0) notFound(); // If multiple matches and the user hasn't disambiguated, show a picker. if (!scope && siblings.length > 1) { return ( } /> {siblings.map((s, i) => { const params = new URLSearchParams({ scope: s.scope }); if (s.scope === "project" && s.projectKey) { params.set("project", s.projectKey); } return ( 0 ? "border-t border-border" : ""}`} >
{s.scope} {s.projectKey ? ( {s.projectKey} ) : ( applies everywhere )}
); })}
); } const snippet: SnippetWithProjectKey | null = await getSnippet(userId, { name, scope, projectKey: project, groupNames, }); if (!snippet) notFound(); // Authorize: user-scope rows belong solely to their owner; project- // scope rows require rw on the project (or ownership) to edit. let canWrite: boolean; if (snippet.scope === "user") { canWrite = snippet.userId === userId; } else if (snippet.projectId) { const access = await getProjectAccess(userId, groupNames, snippet.projectId); canWrite = access === "owner" || access === "rw"; } else { canWrite = false; } const isEditing = wantsEdit && canWrite; // Editor name for "Last edited by ..." footer. const editorRow = snippet.lastEditedBy ? await db .select({ name: users.name, email: users.email }) .from(users) .where(eq(users.id, snippet.lastEditedBy)) .limit(1) : []; const editorLabel = editorRow[0] ? editorRow[0].name ?? editorRow[0].email ?? "unknown" : null; return ( {snippet.scope} {snippet.projectKey ? ` · ${snippet.projectKey}` : ""} } actions={ <> {!isEditing && canWrite ? ( ) : null} } /> {snippet.scope} {snippet.projectKey ? {snippet.projectKey} : null} · Created {new Date(snippet.createdAt).toLocaleString()} {snippet.updatedAt.getTime() !== snippet.createdAt.getTime() ? ( · Updated {new Date(snippet.updatedAt).toLocaleString()} ) : null} {editorLabel && snippet.lastEditedBy !== snippet.userId ? ( · Last edited by {editorLabel} ) : null} {isEditing ? (
{snippet.scope === "project" && snippet.projectKey ? ( ) : null}