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" > - + - + - +