From 83aab4e4287f114223e88f8076151ad43d0c2d6e Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sat, 31 Jan 2026 16:51:21 +1100 Subject: [PATCH] Fix permissions --- app/(api)/api/blob/confirm/route.ts | 7 +- app/(api)/api/blob/presign/route.ts | 5 +- .../calendar/[ownerId]/[projectId]/route.ts | 4 +- app/(api)/api/cron/blob-cleanup/route.ts | 39 ++++++ app/(api)/api/trpc/[trpc]/route.ts | 2 +- app/(auth)/accept-invite/page.tsx | 6 +- app/(auth)/sign-in/page.tsx | 12 +- .../[tenant]/projects/[projectId]/layout.tsx | 4 +- .../[tenant]/projects/[projectId]/page.tsx | 10 +- .../projects/[projectId]/settings/page.tsx | 6 +- .../tasklists/[tasklistId]/page.tsx | 6 +- .../projects/[projectId]/tasklists/page.tsx | 10 +- app/api/auth/[...all]/route.ts | 2 +- app/page.tsx | 12 +- components/auth/otp-verification-form.tsx | 4 +- components/core/cmd-menu.tsx | 2 +- components/core/search-panel.tsx | 2 +- components/editor/mention-suggestion-menu.tsx | 2 +- components/form/editable-date.tsx | 2 +- components/form/event.tsx | 10 +- components/form/notes-form.tsx | 10 +- components/form/shared.tsx | 2 +- components/form/task.tsx | 2 +- components/layout/header.tsx | 2 +- components/layout/navbar.tsx | 2 +- components/layout/page-title.tsx | 2 +- components/project/comment/comment.tsx | 6 +- components/project/comment/comments.tsx | 3 + .../project/events/date-time-picker.tsx | 3 +- components/project/events/full-calendar.tsx | 16 +-- components/project/events/week-calendar.tsx | 4 +- components/settings/team-settings.tsx | 22 +-- drizzle/auth-schema.ts | 4 +- drizzle/schema.ts | 27 ++-- hooks/use-project-prefetch.ts | 2 +- hooks/use-tasklist.tsx | 4 +- hooks/use-tasks.tsx | 5 +- lib/auth/client.ts | 2 +- lib/auth/index.ts | 2 +- lib/blobStore/cleanup.ts | 68 +++++++++ proxy.ts | 2 +- trpc/init.ts | 2 +- trpc/routers/events.ts | 8 ++ trpc/routers/permissions.ts | 74 +++++++--- trpc/routers/posts.ts | 8 ++ trpc/routers/projects.ts | 130 ++++++++++++++++-- trpc/routers/search.ts | 7 +- trpc/routers/settings.ts | 33 ++++- trpc/routers/tasks.ts | 113 ++++++++++++--- trpc/routers/user.ts | 34 ++++- vercel.json | 8 ++ 51 files changed, 584 insertions(+), 170 deletions(-) create mode 100644 app/(api)/api/cron/blob-cleanup/route.ts create mode 100644 lib/blobStore/cleanup.ts create mode 100644 vercel.json diff --git a/app/(api)/api/blob/confirm/route.ts b/app/(api)/api/blob/confirm/route.ts index f28c48fa..45bd2906 100644 --- a/app/(api)/api/blob/confirm/route.ts +++ b/app/(api)/api/blob/confirm/route.ts @@ -1,5 +1,5 @@ -import mime from "mime-types"; import { eq } from "drizzle-orm"; +import mime from "mime-types"; import { type NextRequest, NextResponse } from "next/server"; import { blob } from "@/drizzle/schema"; import { headObject } from "@/lib/blobStore"; @@ -17,10 +17,7 @@ export async function POST(request: NextRequest) { const { fileId } = body as { fileId: string }; if (!fileId) { - return NextResponse.json( - { error: "Missing fileId" }, - { status: 400 }, - ); + return NextResponse.json({ error: "Missing fileId" }, { status: 400 }); } const db = database(); diff --git a/app/(api)/api/blob/presign/route.ts b/app/(api)/api/blob/presign/route.ts index 3a94edbe..fe6dbe2a 100644 --- a/app/(api)/api/blob/presign/route.ts +++ b/app/(api)/api/blob/presign/route.ts @@ -38,7 +38,10 @@ export async function POST(request: NextRequest) { } if (!ALLOWED_TYPES.includes(contentType)) { - return NextResponse.json({ error: "File type not allowed" }, { status: 400 }); + return NextResponse.json( + { error: "File type not allowed" }, + { status: 400 }, + ); } if (contentSize > MAX_FILE_SIZE) { diff --git a/app/(api)/api/calendar/[ownerId]/[projectId]/route.ts b/app/(api)/api/calendar/[ownerId]/[projectId]/route.ts index 1def26c8..697e20fb 100644 --- a/app/(api)/api/calendar/[ownerId]/[projectId]/route.ts +++ b/app/(api)/api/calendar/[ownerId]/[projectId]/route.ts @@ -1,8 +1,8 @@ -import { calendarEvent, project, task, taskList, user } from "@/drizzle/schema"; -import { database } from "@/lib/utils/useDatabase"; import { and, desc, eq, lte } from "drizzle-orm"; import ical, { ICalCalendarMethod } from "ical-generator"; import type { NextRequest } from "next/server"; +import { calendarEvent, project, task, taskList, user } from "@/drizzle/schema"; +import { database } from "@/lib/utils/useDatabase"; export const revalidate = 0; export const dynamic = "force-dynamic"; diff --git a/app/(api)/api/cron/blob-cleanup/route.ts b/app/(api)/api/cron/blob-cleanup/route.ts new file mode 100644 index 00000000..f8671b9a --- /dev/null +++ b/app/(api)/api/cron/blob-cleanup/route.ts @@ -0,0 +1,39 @@ +import { and, eq, lt } from "drizzle-orm"; +import { type NextRequest, NextResponse } from "next/server"; +import { blob } from "@/drizzle/schema"; +import { deleteFile } from "@/lib/blobStore"; +import { database } from "@/lib/utils/useDatabase"; + +export async function GET(request: NextRequest) { + const authHeader = request.headers.get("authorization"); + if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const db = database(); + const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); + + const stalePending = await db + .select() + .from(blob) + .where(and(eq(blob.status, "pending"), lt(blob.createdAt, oneHourAgo))); + + const results = await Promise.allSettled( + stalePending.map(async (b) => { + await deleteFile(b.key); + await db.delete(blob).where(eq(blob.id, b.id)); + return b.id; + }), + ); + + const deletedCount = results.filter((r) => r.status === "fulfilled").length; + const errors = results + .filter((r): r is PromiseRejectedResult => r.status === "rejected") + .map((r, i) => `Failed to delete blob ${stalePending[i].id}: ${r.reason}`); + + return NextResponse.json({ + found: stalePending.length, + deleted: deletedCount, + errors: errors.length > 0 ? errors : undefined, + }); +} diff --git a/app/(api)/api/trpc/[trpc]/route.ts b/app/(api)/api/trpc/[trpc]/route.ts index d62a7b6e..34f3d81c 100644 --- a/app/(api)/api/trpc/[trpc]/route.ts +++ b/app/(api)/api/trpc/[trpc]/route.ts @@ -1,6 +1,6 @@ +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; import { createTRPCContext } from "@/trpc/init"; import { appRouter } from "@/trpc/routers/_app"; -import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; export const maxDuration = 60; export const dynamic = "force-dynamic"; diff --git a/app/(auth)/accept-invite/page.tsx b/app/(auth)/accept-invite/page.tsx index 879845b4..21d5b152 100644 --- a/app/(auth)/accept-invite/page.tsx +++ b/app/(auth)/accept-invite/page.tsx @@ -1,13 +1,13 @@ "use client"; +import { useRouter, useSearchParams } from "next/navigation"; import { useState } from "react"; -import { useSearchParams, useRouter } from "next/navigation"; +import { toast } from "sonner"; +import { OtpVerificationForm } from "@/components/auth/otp-verification-form"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { authClient, useSession } from "@/lib/auth/client"; -import { toast } from "sonner"; -import { OtpVerificationForm } from "@/components/auth/otp-verification-form"; export default function AcceptInvitePage() { const router = useRouter(); diff --git a/app/(auth)/sign-in/page.tsx b/app/(auth)/sign-in/page.tsx index 8d319a2b..237cb236 100644 --- a/app/(auth)/sign-in/page.tsx +++ b/app/(auth)/sign-in/page.tsx @@ -1,10 +1,11 @@ "use client"; +import { Loader2, Mail } from "lucide-react"; +import { useRouter, useSearchParams } from "next/navigation"; import { useState } from "react"; -import { useSearchParams, useRouter } from "next/navigation"; +import { toast } from "sonner"; +import { OtpVerificationForm } from "@/components/auth/otp-verification-form"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Card, CardContent, @@ -12,10 +13,9 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { authClient } from "@/lib/auth/client"; -import { toast } from "sonner"; -import { Loader2, Mail } from "lucide-react"; -import { OtpVerificationForm } from "@/components/auth/otp-verification-form"; export default function SignInPage() { const router = useRouter(); diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/layout.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/layout.tsx index eaac17f8..ac61f820 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/layout.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/layout.tsx @@ -1,8 +1,8 @@ "use client"; -import { TaskListsProvider } from "@/hooks/use-tasklist"; -import { useProjectPrefetch } from "@/hooks/use-project-prefetch"; import { useParams } from "next/navigation"; +import { useProjectPrefetch } from "@/hooks/use-project-prefetch"; +import { TaskListsProvider } from "@/hooks/use-tasklist"; export default function Layout({ children }: { children: React.ReactNode }) { const params = useParams(); diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/page.tsx index b86c6519..bd47ef23 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/page.tsx @@ -1,5 +1,9 @@ "use client"; +import { useMutation, useQueries, useQueryClient } from "@tanstack/react-query"; +import { CalendarIcon, ListIcon } from "lucide-react"; +import Link from "next/link"; +import { useParams, useRouter } from "next/navigation"; import EmptyState from "@/components/core/empty-state"; import { PageLoading } from "@/components/core/loaders"; import PageSection from "@/components/core/section"; @@ -11,14 +15,10 @@ import { CommentsSection } from "@/components/project/comment/comments-section"; import WeekCalendar from "@/components/project/events/week-calendar"; import { TaskListHeader } from "@/components/project/tasklist/tasklist-header"; import { buttonVariants } from "@/components/ui/button"; +import { TaskStatus } from "@/drizzle/types"; import { toStartOfDay } from "@/lib/utils/date"; import { displayMutationError } from "@/lib/utils/error"; import { useTRPC } from "@/trpc/client"; -import { useMutation, useQueries, useQueryClient } from "@tanstack/react-query"; -import { CalendarIcon, ListIcon } from "lucide-react"; -import Link from "next/link"; -import { useParams, useRouter } from "next/navigation"; -import { TaskStatus } from "@/drizzle/types"; export default function ProjectDetails() { const router = useRouter(); diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/settings/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/settings/page.tsx index e17adbcd..936143c4 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/settings/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/settings/page.tsx @@ -1,13 +1,13 @@ "use client"; +import { useQuery } from "@tanstack/react-query"; +import { Settings2, Shield } from "lucide-react"; +import { useParams } from "next/navigation"; import { PageLoading } from "@/components/core/loaders"; import PermissionsManagement from "@/components/core/permissions-management"; import PageSection from "@/components/core/section"; import PageTitle from "@/components/layout/page-title"; import { useTRPC } from "@/trpc/client"; -import { useQuery } from "@tanstack/react-query"; -import { Shield, Settings2 } from "lucide-react"; -import { useParams } from "next/navigation"; export default function ProjectSettings() { const params = useParams(); diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/[tasklistId]/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/[tasklistId]/page.tsx index 3d6185fb..d82ed849 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/[tasklistId]/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/[tasklistId]/page.tsx @@ -1,5 +1,8 @@ "use client"; +import { useQueries } from "@tanstack/react-query"; +import { useParams } from "next/navigation"; +import { parseAsBoolean, useQueryState } from "nuqs"; import { PageLoading } from "@/components/core/loaders"; import PageSection from "@/components/core/section"; import { ConfirmButton } from "@/components/form/button"; @@ -13,9 +16,6 @@ import { useTaskLists } from "@/hooks/use-tasklist"; import { TasksProvider } from "@/hooks/use-tasks"; import { toStartOfDay } from "@/lib/utils/date"; import { useTRPC } from "@/trpc/client"; -import { useQueries } from "@tanstack/react-query"; -import { useParams } from "next/navigation"; -import { parseAsBoolean, useQueryState } from "nuqs"; export default function TaskLists() { const { projectId, tasklistId } = useParams(); diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/page.tsx b/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/page.tsx index c1836273..cb2f8eed 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/page.tsx +++ b/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/page.tsx @@ -1,5 +1,10 @@ "use client"; +import { Title } from "@radix-ui/react-dialog"; +import { useQuery } from "@tanstack/react-query"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { parseAsBoolean, useQueryState } from "nuqs"; import EmptyState from "@/components/core/empty-state"; import { Panel } from "@/components/core/panel"; import PageSection from "@/components/core/section"; @@ -12,11 +17,6 @@ import { TaskListStatus } from "@/drizzle/types"; import { useTaskLists } from "@/hooks/use-tasklist"; import { TasksProvider } from "@/hooks/use-tasks"; import { useTRPC } from "@/trpc/client"; -import { Title } from "@radix-ui/react-dialog"; -import { useQuery } from "@tanstack/react-query"; -import Link from "next/link"; -import { useParams } from "next/navigation"; -import { parseAsBoolean, useQueryState } from "nuqs"; export default function TaskLists() { const { projectId, tenant } = useParams(); diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts index 5b67b064..83ab371a 100644 --- a/app/api/auth/[...all]/route.ts +++ b/app/api/auth/[...all]/route.ts @@ -1,4 +1,4 @@ -import { auth } from "@/lib/auth"; import { toNextJsHandler } from "better-auth/next-js"; +import { auth } from "@/lib/auth"; export const { GET, POST } = toNextJsHandler(auth); diff --git a/app/page.tsx b/app/page.tsx index 1059c2ad..0e34ec0b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -59,11 +59,7 @@ export default async function Home() { href="https://railway.com/deploy/manage" className="inline-flex items-center gap-2 px-6 py-3 rounded-full text-lg font-semibold bg-violet-600 text-white shadow-sm shadow-violet-600/25 border-b-4 border-violet-700 hover:bg-violet-500 hover:border-violet-600 active:border-violet-600 active:shadow-sm active:translate-y-0.5 transition-all duration-150" > - + - + - +