Explorar o código

Restructure API routes into clear subdirectories (#15)

* Move public routes under "/api/public/"

* restructure the api routes

* reorganize api routes

* reqrite url
Bernt Christian Egeland hai 7 meses
pai
achega
47e7b479bd
Modificáronse 58 ficheiros con 87 adicións e 65 borrados
  1. 9 0
      next.config.ts
  2. 4 4
      src/__tests__/features/public/inspection-view.test.tsx
  3. 5 5
      src/__tests__/features/public/invoice-view.test.tsx
  4. 3 3
      src/__tests__/features/public/quote-view.test.tsx
  5. 1 1
      src/app/(authenticated)/settings/company/company-settings.tsx
  6. 5 5
      src/app/(authenticated)/settings/data/data-settings.tsx
  7. 2 2
      src/app/(public)/share/inspection/[orgId]/[token]/inspection-view.tsx
  8. 2 2
      src/app/(public)/share/inspection/[orgId]/[token]/page.tsx
  9. 3 3
      src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx
  10. 3 3
      src/app/(public)/share/invoice/[orgId]/[token]/page.tsx
  11. 1 1
      src/app/(public)/share/quote/[orgId]/[token]/page.tsx
  12. 2 2
      src/app/(public)/share/quote/[orgId]/[token]/quote-view.tsx
  13. 0 0
      src/app/api/internal/cron/recurring-invoices/route.ts
  14. 0 0
      src/app/api/internal/cron/validate-subscriptions/route.ts
  15. 0 0
      src/app/api/protected/backup/export/route.ts
  16. 1 1
      src/app/api/protected/backup/import-invoice-ninja/route.ts
  17. 1 1
      src/app/api/protected/backup/import-lubelog/route.ts
  18. 8 4
      src/app/api/protected/backup/import/route.ts
  19. 0 0
      src/app/api/protected/fetch-metadata/route.ts
  20. 0 0
      src/app/api/protected/files/[...path]/route.ts
  21. 0 0
      src/app/api/protected/inspections/[id]/pdf/route.ts
  22. 0 0
      src/app/api/protected/ping/route.ts
  23. 0 0
      src/app/api/protected/quotes/[id]/pdf/route.ts
  24. 0 0
      src/app/api/protected/services/[id]/pdf/route.ts
  25. 0 0
      src/app/api/protected/subscription/checkout/route.ts
  26. 1 1
      src/app/api/protected/upload/inventory/route.ts
  27. 1 1
      src/app/api/protected/upload/logo/route.ts
  28. 1 1
      src/app/api/protected/upload/route.ts
  29. 1 1
      src/app/api/protected/upload/service-files/route.ts
  30. 0 0
      src/app/api/protected/ws/route.ts
  31. 5 5
      src/app/api/public/auth/[...all]/route.ts
  32. 0 0
      src/app/api/public/files/[token]/[...path]/route.ts
  33. 0 0
      src/app/api/public/forms/inspection-quote-request/route.ts
  34. 0 0
      src/app/api/public/forms/quote-response/route.ts
  35. 0 0
      src/app/api/public/share/inspection/[orgId]/[token]/pdf/route.ts
  36. 0 0
      src/app/api/public/share/invoice/[orgId]/[token]/checkout/route.ts
  37. 0 0
      src/app/api/public/share/invoice/[orgId]/[token]/pdf/route.ts
  38. 0 0
      src/app/api/public/share/invoice/[orgId]/[token]/verify/route.ts
  39. 0 0
      src/app/api/public/share/quote/[orgId]/[token]/pdf/route.ts
  40. 0 0
      src/app/api/v1/health/route.ts
  41. 1 1
      src/app/robots.ts
  42. 1 1
      src/components/online-tracker.tsx
  43. 2 2
      src/features/inspections/Components/InspectionPageClient.tsx
  44. 1 1
      src/features/inspections/Components/QuoteRequestDialog.tsx
  45. 2 2
      src/features/inventory/Components/InventoryPartForm.tsx
  46. 1 1
      src/features/notifications/hooks/useNotificationWebSocket.ts
  47. 1 1
      src/features/quotes/Components/QuotePageClient.tsx
  48. 1 1
      src/features/subscription/Components/subscription-settings.tsx
  49. 1 1
      src/features/vehicles/Actions/deleteUploadedFile.ts
  50. 1 1
      src/features/vehicles/Components/VehicleForm.tsx
  51. 1 1
      src/features/vehicles/Components/service-documents-manager.tsx
  52. 1 1
      src/features/vehicles/Components/service-images-manager.tsx
  53. 1 1
      src/features/vehicles/Components/service-page/ServicePageClient.tsx
  54. 1 1
      src/features/vehicles/Components/service-video-manager.tsx
  55. 1 0
      src/lib/auth-client.ts
  56. 1 0
      src/lib/auth.ts
  57. 9 2
      src/lib/resolve-upload-path.ts
  58. 1 1
      src/proxy.ts

+ 9 - 0
next.config.ts

@@ -9,6 +9,15 @@ const nextConfig: NextConfig = {
   experimental: {
   experimental: {
     proxyClientMaxBodySize: '2gb',
     proxyClientMaxBodySize: '2gb',
   },
   },
+  async rewrites() {
+    return [
+      // Old /api/files/ URLs stored in the DB before the protected/ restructure
+      {
+        source: '/api/files/:path*',
+        destination: '/api/protected/files/:path*',
+      },
+    ]
+  },
 }
 }
 
 
 export default nextConfig
 export default nextConfig

+ 4 - 4
src/__tests__/features/public/inspection-view.test.tsx

@@ -275,7 +275,7 @@ describe("InspectionView", () => {
   });
   });
 
 
   describe("cancel quote request", () => {
   describe("cancel quote request", () => {
-    it("calls DELETE /api/public/inspection-quote-request with correct body", async () => {
+    it("calls DELETE /api/public/forms/inspection-quote-request with correct body", async () => {
       mockFetch.mockResolvedValue({
       mockFetch.mockResolvedValue({
         ok: true,
         ok: true,
         json: () => Promise.resolve({ success: true }),
         json: () => Promise.resolve({ success: true }),
@@ -285,7 +285,7 @@ describe("InspectionView", () => {
 
 
       await waitFor(() => {
       await waitFor(() => {
         expect(mockFetch).toHaveBeenCalledWith(
         expect(mockFetch).toHaveBeenCalledWith(
-          "/api/public/inspection-quote-request",
+          "/api/public/forms/inspection-quote-request",
           expect.objectContaining({ method: "DELETE" })
           expect.objectContaining({ method: "DELETE" })
         );
         );
         const body = JSON.parse(mockFetch.mock.calls[0][1].body);
         const body = JSON.parse(mockFetch.mock.calls[0][1].body);
@@ -339,7 +339,7 @@ describe("InspectionView", () => {
       render(<InspectionView {...DEFAULT_PROPS} />);
       render(<InspectionView {...DEFAULT_PROPS} />);
       await userEvent.click(screen.getByRole("button", { name: /download pdf/i }));
       await userEvent.click(screen.getByRole("button", { name: /download pdf/i }));
       expect(mockWindowOpen).toHaveBeenCalledWith(
       expect(mockWindowOpen).toHaveBeenCalledWith(
-        "/api/share/inspection/org-1/pub-tok-insp/pdf",
+        "/api/public/share/inspection/org-1/pub-tok-insp/pdf",
         "_blank"
         "_blank"
       );
       );
     });
     });
@@ -368,7 +368,7 @@ describe("InspectionView", () => {
     const IMG_ITEM = {
     const IMG_ITEM = {
       ...PASS_ITEM,
       ...PASS_ITEM,
       name: "Oil Level",
       name: "Oil Level",
-      imageUrls: ["/api/files/public/tok/services/photo1.jpg"],
+      imageUrls: ["/api/public/files/tok/services/photo1.jpg"],
     };
     };
 
 
     it("renders inspection image thumbnails", () => {
     it("renders inspection image thumbnails", () => {

+ 5 - 5
src/__tests__/features/public/invoice-view.test.tsx

@@ -265,7 +265,7 @@ describe("InvoiceView", () => {
 
 
       await waitFor(() => {
       await waitFor(() => {
         expect(mockFetch).toHaveBeenCalledWith(
         expect(mockFetch).toHaveBeenCalledWith(
-          "/api/share/invoice/org-1/tok-xyz/checkout",
+          "/api/public/share/invoice/org-1/tok-xyz/checkout",
           expect.objectContaining({ method: "POST" })
           expect.objectContaining({ method: "POST" })
         );
         );
         const body = JSON.parse(mockFetch.mock.calls[0][1].body);
         const body = JSON.parse(mockFetch.mock.calls[0][1].body);
@@ -304,7 +304,7 @@ describe("InvoiceView", () => {
         expect(screen.getByText(/payment received/i)).toBeInTheDocument();
         expect(screen.getByText(/payment received/i)).toBeInTheDocument();
       });
       });
       expect(mockFetch).toHaveBeenCalledWith(
       expect(mockFetch).toHaveBeenCalledWith(
-        "/api/share/invoice/org-1/tok-xyz/verify",
+        "/api/public/share/invoice/org-1/tok-xyz/verify",
         expect.objectContaining({
         expect.objectContaining({
           method: "POST",
           method: "POST",
           body: expect.stringContaining('"provider":"stripe"'),
           body: expect.stringContaining('"provider":"stripe"'),
@@ -349,7 +349,7 @@ describe("InvoiceView", () => {
     const IMAGE_ATTACHMENT = {
     const IMAGE_ATTACHMENT = {
       id: "att-1",
       id: "att-1",
       fileName: "service-photo.jpg",
       fileName: "service-photo.jpg",
-      fileUrl: "/api/files/public/tok-xyz/services/service-photo.jpg",
+      fileUrl: "/api/public/files/tok-xyz/services/service-photo.jpg",
       fileType: "image/jpeg",
       fileType: "image/jpeg",
       fileSize: 50000,
       fileSize: 50000,
       category: "services",
       category: "services",
@@ -408,7 +408,7 @@ describe("InvoiceView", () => {
       const pdfAttachment = {
       const pdfAttachment = {
         id: "att-pdf",
         id: "att-pdf",
         fileName: "diagnostic.pdf",
         fileName: "diagnostic.pdf",
-        fileUrl: "/api/files/public/tok-xyz/services/diagnostic.pdf",
+        fileUrl: "/api/public/files/tok-xyz/services/diagnostic.pdf",
         fileType: "application/pdf",
         fileType: "application/pdf",
         fileSize: 20000,
         fileSize: 20000,
         category: "services",
         category: "services",
@@ -433,7 +433,7 @@ describe("InvoiceView", () => {
       await userEvent.click(screen.getByRole("button", { name: /download pdf/i }));
       await userEvent.click(screen.getByRole("button", { name: /download pdf/i }));
 
 
       await waitFor(() => {
       await waitFor(() => {
-        expect(mockFetch).toHaveBeenCalledWith("/api/share/invoice/org-1/tok-xyz/pdf");
+        expect(mockFetch).toHaveBeenCalledWith("/api/public/share/invoice/org-1/tok-xyz/pdf");
       });
       });
     });
     });
   });
   });

+ 3 - 3
src/__tests__/features/public/quote-view.test.tsx

@@ -223,7 +223,7 @@ describe("QuoteView", () => {
   });
   });
 
 
   describe("Accept Quote interaction", () => {
   describe("Accept Quote interaction", () => {
-    it("calls /api/public/quote-response with action=accepted", async () => {
+    it("calls /api/public/forms/quote-response with action=accepted", async () => {
       mockFetch.mockResolvedValue({
       mockFetch.mockResolvedValue({
         ok: true,
         ok: true,
         json: () => Promise.resolve({ success: true }),
         json: () => Promise.resolve({ success: true }),
@@ -233,7 +233,7 @@ describe("QuoteView", () => {
 
 
       await waitFor(() => {
       await waitFor(() => {
         expect(mockFetch).toHaveBeenCalledWith(
         expect(mockFetch).toHaveBeenCalledWith(
-          "/api/public/quote-response",
+          "/api/public/forms/quote-response",
           expect.objectContaining({ method: "POST" })
           expect.objectContaining({ method: "POST" })
         );
         );
         const body = JSON.parse(mockFetch.mock.calls[0][1].body);
         const body = JSON.parse(mockFetch.mock.calls[0][1].body);
@@ -340,7 +340,7 @@ describe("QuoteView", () => {
       await userEvent.click(screen.getByRole("button", { name: /download pdf/i }));
       await userEvent.click(screen.getByRole("button", { name: /download pdf/i }));
 
 
       await waitFor(() => {
       await waitFor(() => {
-        expect(mockFetch).toHaveBeenCalledWith("/api/share/quote/org-1/tok-abc/pdf");
+        expect(mockFetch).toHaveBeenCalledWith("/api/public/share/quote/org-1/tok-abc/pdf");
       });
       });
     });
     });
 
 

+ 1 - 1
src/app/(authenticated)/settings/company/company-settings.tsx

@@ -67,7 +67,7 @@ export function CompanySettings({ settings, organizationName }: { settings: Reco
     try {
     try {
       const formData = new FormData();
       const formData = new FormData();
       formData.append("file", file);
       formData.append("file", file);
-      const res = await fetch("/api/upload/logo", { method: "POST", body: formData });
+      const res = await fetch("/api/protected/upload/logo", { method: "POST", body: formData });
       if (!res.ok) {
       if (!res.ok) {
         const err = await res.json();
         const err = await res.json();
         toast.error(err.error || "Failed to upload logo", { id: toastId });
         toast.error(err.error || "Failed to upload logo", { id: toastId });

+ 5 - 5
src/app/(authenticated)/settings/data/data-settings.tsx

@@ -86,7 +86,7 @@ export function DataSettings() {
   const handleExport = async () => {
   const handleExport = async () => {
     setExporting(true)
     setExporting(true)
     try {
     try {
-      const res = await fetch('/api/backup/export', {
+      const res = await fetch('/api/protected/backup/export', {
         method: 'POST',
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
         headers: { 'Content-Type': 'application/json' },
         body: JSON.stringify({ include: options }),
         body: JSON.stringify({ include: options }),
@@ -123,7 +123,7 @@ export function DataSettings() {
 
 
       if (isZip) {
       if (isZip) {
         const buffer = await selectedFile.arrayBuffer()
         const buffer = await selectedFile.arrayBuffer()
-        res = await fetch('/api/backup/import', {
+        res = await fetch('/api/protected/backup/import', {
           method: 'POST',
           method: 'POST',
           headers: { 'Content-Type': 'application/zip' },
           headers: { 'Content-Type': 'application/zip' },
           body: buffer,
           body: buffer,
@@ -131,7 +131,7 @@ export function DataSettings() {
       } else {
       } else {
         const text = await selectedFile.text()
         const text = await selectedFile.text()
         const json = JSON.parse(text)
         const json = JSON.parse(text)
-        res = await fetch('/api/backup/import', {
+        res = await fetch('/api/protected/backup/import', {
           method: 'POST',
           method: 'POST',
           headers: { 'Content-Type': 'application/json' },
           headers: { 'Content-Type': 'application/json' },
           body: JSON.stringify(json),
           body: JSON.stringify(json),
@@ -165,7 +165,7 @@ export function DataSettings() {
     setImportingLubelog(true)
     setImportingLubelog(true)
     try {
     try {
       const buffer = await lubelogFile.arrayBuffer()
       const buffer = await lubelogFile.arrayBuffer()
-      const res = await fetch('/api/backup/import-lubelog', {
+      const res = await fetch('/api/protected/backup/import-lubelog', {
         method: 'POST',
         method: 'POST',
         headers: { 'Content-Type': 'application/zip' },
         headers: { 'Content-Type': 'application/zip' },
         body: buffer,
         body: buffer,
@@ -204,7 +204,7 @@ export function DataSettings() {
     setImportingInvoiceNinja(true)
     setImportingInvoiceNinja(true)
     try {
     try {
       const buffer = await invoiceNinjaFile.arrayBuffer()
       const buffer = await invoiceNinjaFile.arrayBuffer()
-      const res = await fetch('/api/backup/import-invoice-ninja', {
+      const res = await fetch('/api/protected/backup/import-invoice-ninja', {
         method: 'POST',
         method: 'POST',
         headers: { 'Content-Type': 'application/zip' },
         headers: { 'Content-Type': 'application/zip' },
         body: buffer,
         body: buffer,

+ 2 - 2
src/app/(public)/share/inspection/[orgId]/[token]/inspection-view.tsx

@@ -101,7 +101,7 @@ export function InspectionView({
   const handleCancelQuoteRequest = async () => {
   const handleCancelQuoteRequest = async () => {
     setIsCancelling(true);
     setIsCancelling(true);
     try {
     try {
-      const res = await fetch("/api/public/inspection-quote-request", {
+      const res = await fetch("/api/public/forms/inspection-quote-request", {
         method: "DELETE",
         method: "DELETE",
         headers: { "Content-Type": "application/json" },
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({ inspectionId: inspection.id, publicToken }),
         body: JSON.stringify({ inspectionId: inspection.id, publicToken }),
@@ -343,7 +343,7 @@ export function InspectionView({
         <Button
         <Button
           variant="outline"
           variant="outline"
           className="gap-2"
           className="gap-2"
-          onClick={() => window.open(`/api/share/inspection/${orgId}/${publicToken}/pdf`, "_blank")}
+          onClick={() => window.open(`/api/public/share/inspection/${orgId}/${publicToken}/pdf`, "_blank")}
         >
         >
           <Download className="h-4 w-4" />
           <Download className="h-4 w-4" />
           Download PDF
           Download PDF

+ 2 - 2
src/app/(public)/share/inspection/[orgId]/[token]/page.tsx

@@ -89,7 +89,7 @@ export default async function PublicInspectionPage({
   let logoUrl = "";
   let logoUrl = "";
   if (rawLogoUrl) {
   if (rawLogoUrl) {
     const match = rawLogoUrl.match(/^\/api\/files\/[^/]+\/(.+)$/);
     const match = rawLogoUrl.match(/^\/api\/files\/[^/]+\/(.+)$/);
-    if (match) logoUrl = `/api/files/public/${token}/${match[1]}`;
+    if (match) logoUrl = `/api/public/files/${token}/${match[1]}`;
     else logoUrl = rawLogoUrl;
     else logoUrl = rawLogoUrl;
   }
   }
 
 
@@ -102,7 +102,7 @@ export default async function PublicInspectionPage({
       ...item,
       ...item,
       imageUrls: item.imageUrls.map((url) => {
       imageUrls: item.imageUrls.map((url) => {
         const match = url.match(/^\/api\/files\/[^/]+\/(.+)$/);
         const match = url.match(/^\/api\/files\/[^/]+\/(.+)$/);
-        if (match) return `/api/files/public/${token}/${match[1]}`;
+        if (match) return `/api/public/files/${token}/${match[1]}`;
         return url;
         return url;
       }),
       }),
     })),
     })),

+ 3 - 3
src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx

@@ -211,7 +211,7 @@ export function InvoiceView({
     async (provider: string, externalId: string) => {
     async (provider: string, externalId: string) => {
       setVerifying(true)
       setVerifying(true)
       try {
       try {
-        const res = await fetch(`/api/share/invoice/${orgId}/${token}/verify`, {
+        const res = await fetch(`/api/public/share/invoice/${orgId}/${token}/verify`, {
           method: 'POST',
           method: 'POST',
           headers: { 'Content-Type': 'application/json' },
           headers: { 'Content-Type': 'application/json' },
           body: JSON.stringify({ provider, externalId }),
           body: JSON.stringify({ provider, externalId }),
@@ -251,7 +251,7 @@ export function InvoiceView({
   }, [verifyPayment])
   }, [verifyPayment])
 
 
   const handleDownloadPDF = async () => {
   const handleDownloadPDF = async () => {
-    const res = await fetch(`/api/share/invoice/${orgId}/${token}/pdf`)
+    const res = await fetch(`/api/public/share/invoice/${orgId}/${token}/pdf`)
     if (!res.ok) return
     if (!res.ok) return
     const blob = await res.blob()
     const blob = await res.blob()
     const url = URL.createObjectURL(blob)
     const url = URL.createObjectURL(blob)
@@ -276,7 +276,7 @@ export function InvoiceView({
 
 
     setPaymentLoading(provider)
     setPaymentLoading(provider)
     try {
     try {
-      const res = await fetch(`/api/share/invoice/${orgId}/${token}/checkout`, {
+      const res = await fetch(`/api/public/share/invoice/${orgId}/${token}/checkout`, {
         method: 'POST',
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
         headers: { 'Content-Type': 'application/json' },
         body: JSON.stringify({ provider, amount }),
         body: JSON.stringify({ provider, amount }),

+ 3 - 3
src/app/(public)/share/invoice/[orgId]/[token]/page.tsx

@@ -4,10 +4,10 @@ import { InvoiceView } from "./invoice-view";
 import { getFeatures } from "@/lib/features";
 import { getFeatures } from "@/lib/features";
 import type { Metadata } from "next";
 import type { Metadata } from "next";
 
 
-/** Rewrites /api/files/[orgId]/[category]/[filename] to /api/files/public/[token]/[category]/[filename] */
+/** Rewrites /api/protected/files/[orgId]/[category]/[filename] to /api/public/files/[token]/[category]/[filename] */
 function toPublicFileUrl(fileUrl: string, token: string): string {
 function toPublicFileUrl(fileUrl: string, token: string): string {
-  const match = fileUrl.match(/^\/api\/files\/[^/]+\/(.+)$/);
-  if (match) return `/api/files/public/${token}/${match[1]}`;
+  const match = fileUrl.match(/^\/api\/protected\/files\/[^/]+\/(.+)$/);
+  if (match) return `/api/public/files/${token}/${match[1]}`;
   // Legacy URLs pass through as-is
   // Legacy URLs pass through as-is
   return fileUrl;
   return fileUrl;
 }
 }

+ 1 - 1
src/app/(public)/share/quote/[orgId]/[token]/page.tsx

@@ -92,7 +92,7 @@ export default async function PublicQuotePage({
   let logoUrl = "";
   let logoUrl = "";
   if (rawLogoUrl) {
   if (rawLogoUrl) {
     const match = rawLogoUrl.match(/^\/api\/files\/[^/]+\/(.+)$/);
     const match = rawLogoUrl.match(/^\/api\/files\/[^/]+\/(.+)$/);
-    if (match) logoUrl = `/api/files/public/${token}/${match[1]}`;
+    if (match) logoUrl = `/api/public/files/${token}/${match[1]}`;
     else logoUrl = rawLogoUrl;
     else logoUrl = rawLogoUrl;
   }
   }
 
 

+ 2 - 2
src/app/(public)/share/quote/[orgId]/[token]/quote-view.tsx

@@ -101,7 +101,7 @@ export function QuoteView({
   const handleDownloadPDF = async () => {
   const handleDownloadPDF = async () => {
     setDownloading(true);
     setDownloading(true);
     try {
     try {
-      const res = await fetch(`/api/share/quote/${orgId}/${token}/pdf`);
+      const res = await fetch(`/api/public/share/quote/${orgId}/${token}/pdf`);
       if (!res.ok) throw new Error("Failed");
       if (!res.ok) throw new Error("Failed");
       const blob = await res.blob();
       const blob = await res.blob();
       const url = URL.createObjectURL(blob);
       const url = URL.createObjectURL(blob);
@@ -119,7 +119,7 @@ export function QuoteView({
   const handleQuoteResponse = async (action: "accepted" | "changes_requested", message?: string) => {
   const handleQuoteResponse = async (action: "accepted" | "changes_requested", message?: string) => {
     setSubmitting(true);
     setSubmitting(true);
     try {
     try {
-      const res = await fetch("/api/public/quote-response", {
+      const res = await fetch("/api/public/forms/quote-response", {
         method: "POST",
         method: "POST",
         headers: { "Content-Type": "application/json" },
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({ quoteId: quote.id, publicToken: token, action, message }),
         body: JSON.stringify({ quoteId: quote.id, publicToken: token, action, message }),

+ 0 - 0
src/app/api/cron/recurring-invoices/route.ts → src/app/api/internal/cron/recurring-invoices/route.ts


+ 0 - 0
src/app/api/cron/validate-subscriptions/route.ts → src/app/api/internal/cron/validate-subscriptions/route.ts


+ 0 - 0
src/app/api/backup/export/route.ts → src/app/api/protected/backup/export/route.ts


+ 1 - 1
src/app/api/backup/import-invoice-ninja/route.ts → src/app/api/protected/backup/import-invoice-ninja/route.ts

@@ -490,7 +490,7 @@ export async function POST(request: NextRequest) {
           const targetPath = path.join(targetDir, filename);
           const targetPath = path.join(targetDir, filename);
           await writeFile(targetPath, fileData);
           await writeFile(targetPath, fileData);
 
 
-          const fileUrl = `/api/files/${organizationId}/services/${filename}`;
+          const fileUrl = `/api/protected/files/${organizationId}/services/${filename}`;
           const isImage = /\.(jpg|jpeg|png|webp)$/i.test(doc.name);
           const isImage = /\.(jpg|jpeg|png|webp)$/i.test(doc.name);
 
 
           await tx.serviceAttachment.create({
           await tx.serviceAttachment.create({

+ 1 - 1
src/app/api/backup/import-lubelog/route.ts → src/app/api/protected/backup/import-lubelog/route.ts

@@ -302,7 +302,7 @@ export async function POST(request: NextRequest) {
       }
       }
 
 
       await writeFile(targetPath, fileData);
       await writeFile(targetPath, fileData);
-      const fileUrl = `/api/files/${organizationId}/${category}/${filename}`;
+      const fileUrl = `/api/protected/files/${organizationId}/${category}/${filename}`;
       return { fileUrl, fileSize: fileData.length };
       return { fileUrl, fileSize: fileData.length };
     }
     }
 
 

+ 8 - 4
src/app/api/backup/import/route.ts → src/app/api/protected/backup/import/route.ts

@@ -104,14 +104,18 @@ function rewriteFileUrl(
   newOrgId: string
   newOrgId: string
 ): string | null {
 ): string | null {
   if (!url) return null;
   if (!url) return null;
-  // New format: /api/files/OLD_ORG_ID/category/filename
+  // New format: /api/protected/files/OLD_ORG_ID/category/filename
+  if (url.startsWith("/api/protected/files/")) {
+    return url.replace(/^\/api\/protected\/files\/[^/]+\//, `/api/protected/files/${newOrgId}/`);
+  }
+  // Old format (pre-restructure): /api/files/OLD_ORG_ID/category/filename
   if (url.startsWith("/api/files/")) {
   if (url.startsWith("/api/files/")) {
-    return url.replace(/^\/api\/files\/[^/]+\//, `/api/files/${newOrgId}/`);
+    return url.replace(/^\/api\/files\/[^/]+\//, `/api/protected/files/${newOrgId}/`);
   }
   }
   // Legacy format: /uploads/category/filename → convert to new format
   // Legacy format: /uploads/category/filename → convert to new format
   if (url.startsWith("/uploads/")) {
   if (url.startsWith("/uploads/")) {
     const relative = url.replace(/^\/uploads\//, "");
     const relative = url.replace(/^\/uploads\//, "");
-    return `/api/files/${newOrgId}/${relative}`;
+    return `/api/protected/files/${newOrgId}/${relative}`;
   }
   }
   return url;
   return url;
 }
 }
@@ -179,7 +183,7 @@ export async function POST(request: NextRequest) {
             (s: Record<string, unknown>) => {
             (s: Record<string, unknown>) => {
               let value = s.value as string;
               let value = s.value as string;
               // Rewrite file URLs in settings (e.g. logo paths)
               // Rewrite file URLs in settings (e.g. logo paths)
-              if (value?.startsWith("/api/files/")) {
+              if (value?.startsWith("/api/protected/files/") || value?.startsWith("/api/files/")) {
                 value = rewriteFileUrl(value, ctx.organizationId) || value;
                 value = rewriteFileUrl(value, ctx.organizationId) || value;
               }
               }
               return {
               return {

+ 0 - 0
src/app/api/fetch-metadata/route.ts → src/app/api/protected/fetch-metadata/route.ts


+ 0 - 0
src/app/api/files/[...path]/route.ts → src/app/api/protected/files/[...path]/route.ts


+ 0 - 0
src/app/api/inspections/[id]/pdf/route.ts → src/app/api/protected/inspections/[id]/pdf/route.ts


+ 0 - 0
src/app/api/ping/route.ts → src/app/api/protected/ping/route.ts


+ 0 - 0
src/app/api/quotes/[id]/pdf/route.ts → src/app/api/protected/quotes/[id]/pdf/route.ts


+ 0 - 0
src/app/api/services/[id]/pdf/route.ts → src/app/api/protected/services/[id]/pdf/route.ts


+ 0 - 0
src/app/api/subscription/checkout/route.ts → src/app/api/protected/subscription/checkout/route.ts


+ 1 - 1
src/app/api/upload/inventory/route.ts → src/app/api/protected/upload/inventory/route.ts

@@ -42,5 +42,5 @@ export async function POST(request: NextRequest) {
   const bytes = new Uint8Array(await file.arrayBuffer());
   const bytes = new Uint8Array(await file.arrayBuffer());
   await writeFile(path.join(uploadDir, filename), bytes);
   await writeFile(path.join(uploadDir, filename), bytes);
 
 
-  return NextResponse.json({ url: `/api/files/${ctx.organizationId}/inventory/${filename}` });
+  return NextResponse.json({ url: `/api/protected/files/${ctx.organizationId}/inventory/${filename}` });
 }
 }

+ 1 - 1
src/app/api/upload/logo/route.ts → src/app/api/protected/upload/logo/route.ts

@@ -38,7 +38,7 @@ export async function POST(request: Request) {
     const buffer = Buffer.from(await file.arrayBuffer());
     const buffer = Buffer.from(await file.arrayBuffer());
     await writeFile(path.join(uploadDir, fileName), buffer);
     await writeFile(path.join(uploadDir, fileName), buffer);
 
 
-    const url = `/api/files/${ctx.organizationId}/logos/${fileName}`;
+    const url = `/api/protected/files/${ctx.organizationId}/logos/${fileName}`;
 
 
     // Delete old logo file if one exists
     // Delete old logo file if one exists
     const oldLogo = await db.appSetting.findFirst({
     const oldLogo = await db.appSetting.findFirst({

+ 1 - 1
src/app/api/upload/route.ts → src/app/api/protected/upload/route.ts

@@ -42,5 +42,5 @@ export async function POST(request: NextRequest) {
   const bytes = new Uint8Array(await file.arrayBuffer());
   const bytes = new Uint8Array(await file.arrayBuffer());
   await writeFile(path.join(uploadDir, filename), bytes);
   await writeFile(path.join(uploadDir, filename), bytes);
 
 
-  return NextResponse.json({ url: `/api/files/${ctx.organizationId}/vehicles/${filename}` });
+  return NextResponse.json({ url: `/api/protected/files/${ctx.organizationId}/vehicles/${filename}` });
 }
 }

+ 1 - 1
src/app/api/upload/service-files/route.ts → src/app/api/protected/upload/service-files/route.ts

@@ -56,7 +56,7 @@ export async function POST(request: NextRequest) {
   await writeFile(path.join(uploadDir, filename), bytes);
   await writeFile(path.join(uploadDir, filename), bytes);
 
 
   return NextResponse.json({
   return NextResponse.json({
-    url: `/api/files/${ctx.organizationId}/services/${filename}`,
+    url: `/api/protected/files/${ctx.organizationId}/services/${filename}`,
     fileName: file.name,
     fileName: file.name,
     fileType: file.type,
     fileType: file.type,
     fileSize: file.size,
     fileSize: file.size,

+ 0 - 0
src/app/api/ws/route.ts → src/app/api/protected/ws/route.ts


+ 5 - 5
src/app/api/auth/[...all]/route.ts → src/app/api/public/auth/[...all]/route.ts

@@ -8,11 +8,11 @@ const { POST: authPOST, GET } = toNextJsHandler(auth);
 // Better-auth registers sub-paths like /sign-in/email, /sign-up/email,
 // Better-auth registers sub-paths like /sign-in/email, /sign-up/email,
 // /two-factor/verify-totp, etc., so we match by prefix.
 // /two-factor/verify-totp, etc., so we match by prefix.
 const strictPrefixes: { prefix: string; limit: number; windowMs: number }[] = [
 const strictPrefixes: { prefix: string; limit: number; windowMs: number }[] = [
-  { prefix: "/api/auth/sign-in", limit: 10, windowMs: 60_000 },
-  { prefix: "/api/auth/two-factor/verify", limit: 10, windowMs: 60_000 },
-  { prefix: "/api/auth/sign-up", limit: 5, windowMs: 60_000 },
-  { prefix: "/api/auth/request-password-reset", limit: 5, windowMs: 60_000 },
-  { prefix: "/api/auth/reset-password", limit: 5, windowMs: 60_000 },
+  { prefix: "/api/public/auth/sign-in", limit: 10, windowMs: 60_000 },
+  { prefix: "/api/public/auth/two-factor/verify", limit: 10, windowMs: 60_000 },
+  { prefix: "/api/public/auth/sign-up", limit: 5, windowMs: 60_000 },
+  { prefix: "/api/public/auth/request-password-reset", limit: 5, windowMs: 60_000 },
+  { prefix: "/api/public/auth/reset-password", limit: 5, windowMs: 60_000 },
 ];
 ];
 
 
 const defaultConfig = { limit: 30, windowMs: 60_000 };
 const defaultConfig = { limit: 30, windowMs: 60_000 };

+ 0 - 0
src/app/api/files/public/[token]/[...path]/route.ts → src/app/api/public/files/[token]/[...path]/route.ts


+ 0 - 0
src/app/api/public/inspection-quote-request/route.ts → src/app/api/public/forms/inspection-quote-request/route.ts


+ 0 - 0
src/app/api/public/quote-response/route.ts → src/app/api/public/forms/quote-response/route.ts


+ 0 - 0
src/app/api/share/inspection/[orgId]/[token]/pdf/route.ts → src/app/api/public/share/inspection/[orgId]/[token]/pdf/route.ts


+ 0 - 0
src/app/api/share/invoice/[orgId]/[token]/checkout/route.ts → src/app/api/public/share/invoice/[orgId]/[token]/checkout/route.ts


+ 0 - 0
src/app/api/share/invoice/[orgId]/[token]/pdf/route.ts → src/app/api/public/share/invoice/[orgId]/[token]/pdf/route.ts


+ 0 - 0
src/app/api/share/invoice/[orgId]/[token]/verify/route.ts → src/app/api/public/share/invoice/[orgId]/[token]/verify/route.ts


+ 0 - 0
src/app/api/share/quote/[orgId]/[token]/pdf/route.ts → src/app/api/public/share/quote/[orgId]/[token]/pdf/route.ts


+ 0 - 0
src/app/api/health/route.ts → src/app/api/v1/health/route.ts


+ 1 - 1
src/app/robots.ts

@@ -5,7 +5,7 @@ export default function robots(): MetadataRoute.Robots {
     rules: [
     rules: [
       {
       {
         userAgent: "*",
         userAgent: "*",
-        disallow: ["/share/invoice/", "/api/share/invoice/"],
+        disallow: ["/share/invoice/", "/api/public/share/invoice/"],
       },
       },
     ],
     ],
   };
   };

+ 1 - 1
src/components/online-tracker.tsx

@@ -5,7 +5,7 @@ export function OnlineTracker() {
   useEffect(() => {
   useEffect(() => {
     const ping = () => {
     const ping = () => {
       if (document.visibilityState === "visible") {
       if (document.visibilityState === "visible") {
-        fetch("/api/ping", { method: "POST" });
+        fetch("/api/protected/ping", { method: "POST" });
       }
       }
     };
     };
 
 

+ 2 - 2
src/features/inspections/Components/InspectionPageClient.tsx

@@ -230,7 +230,7 @@ function InspectionItemRow({
         const formData = new FormData();
         const formData = new FormData();
         formData.append("file", file);
         formData.append("file", file);
 
 
-        const res = await fetch("/api/upload/service-files", {
+        const res = await fetch("/api/protected/upload/service-files", {
           method: "POST",
           method: "POST",
           body: formData,
           body: formData,
         });
         });
@@ -523,7 +523,7 @@ export function InspectionPageClient({
               </DropdownMenuTrigger>
               </DropdownMenuTrigger>
               <DropdownMenuContent align="end">
               <DropdownMenuContent align="end">
                 <DropdownMenuItem
                 <DropdownMenuItem
-                  onClick={() => window.open(`/api/inspections/${inspection.id}/pdf`, "_blank")}
+                  onClick={() => window.open(`/api/protected/inspections/${inspection.id}/pdf`, "_blank")}
                 >
                 >
                   <Download className="mr-2 h-4 w-4" />
                   <Download className="mr-2 h-4 w-4" />
                   Download PDF
                   Download PDF

+ 1 - 1
src/features/inspections/Components/QuoteRequestDialog.tsx

@@ -68,7 +68,7 @@ export function QuoteRequestDialog({
 
 
     setIsSubmitting(true);
     setIsSubmitting(true);
     try {
     try {
-      const res = await fetch("/api/public/inspection-quote-request", {
+      const res = await fetch("/api/public/forms/inspection-quote-request", {
         method: "POST",
         method: "POST",
         headers: { "Content-Type": "application/json" },
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({
         body: JSON.stringify({

+ 2 - 2
src/features/inventory/Components/InventoryPartForm.tsx

@@ -70,7 +70,7 @@ export function InventoryPartForm({ open, onOpenChange, part, markupMultiplier }
       const compressed = await compressImage(file);
       const compressed = await compressImage(file);
       const formData = new FormData();
       const formData = new FormData();
       formData.append("file", compressed);
       formData.append("file", compressed);
-      const res = await fetch("/api/upload/inventory", {
+      const res = await fetch("/api/protected/upload/inventory", {
         method: "POST",
         method: "POST",
         body: formData,
         body: formData,
       });
       });
@@ -120,7 +120,7 @@ export function InventoryPartForm({ open, onOpenChange, part, markupMultiplier }
 
 
     setFetching(true);
     setFetching(true);
     try {
     try {
-      const res = await fetch("/api/fetch-metadata", {
+      const res = await fetch("/api/protected/fetch-metadata", {
         method: "POST",
         method: "POST",
         headers: { "Content-Type": "application/json" },
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({ url: targetUrl }),
         body: JSON.stringify({ url: targetUrl }),

+ 1 - 1
src/features/notifications/hooks/useNotificationWebSocket.ts

@@ -29,7 +29,7 @@ export function useNotificationWebSocket() {
       if (!mountedRef.current) return;
       if (!mountedRef.current) return;
 
 
       const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
       const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
-      const url = `${protocol}//${window.location.host}/api/ws`;
+      const url = `${protocol}//${window.location.host}/api/protected/ws`;
       const ws = new WebSocket(url);
       const ws = new WebSocket(url);
       wsRef.current = ws;
       wsRef.current = ws;
 
 

+ 1 - 1
src/features/quotes/Components/QuotePageClient.tsx

@@ -289,7 +289,7 @@ export function QuotePageClient({
   const handleDownloadPDF = async () => {
   const handleDownloadPDF = async () => {
     setDownloading(true);
     setDownloading(true);
     try {
     try {
-      const res = await fetch(`/api/quotes/${quote.id}/pdf`);
+      const res = await fetch(`/api/protected/quotes/${quote.id}/pdf`);
       if (!res.ok) throw new Error("Failed to generate PDF");
       if (!res.ok) throw new Error("Failed to generate PDF");
       const blob = await res.blob();
       const blob = await res.blob();
       const url = URL.createObjectURL(blob);
       const url = URL.createObjectURL(blob);

+ 1 - 1
src/features/subscription/Components/subscription-settings.tsx

@@ -30,7 +30,7 @@ export function SubscriptionSettings({
   const handleCheckout = async (selectedPlan: "pro" | "enterprise") => {
   const handleCheckout = async (selectedPlan: "pro" | "enterprise") => {
     setCheckoutLoading(selectedPlan);
     setCheckoutLoading(selectedPlan);
     try {
     try {
-      const res = await fetch("/api/subscription/checkout", {
+      const res = await fetch("/api/protected/subscription/checkout", {
         method: "POST",
         method: "POST",
         headers: { "Content-Type": "application/json" },
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({ plan: selectedPlan }),
         body: JSON.stringify({ plan: selectedPlan }),

+ 1 - 1
src/features/vehicles/Actions/deleteUploadedFile.ts

@@ -12,7 +12,7 @@ import { resolveUploadPath } from "@/lib/resolve-upload-path";
 export async function deleteUploadedFile(fileUrl: string) {
 export async function deleteUploadedFile(fileUrl: string) {
   return withAuth(async ({ organizationId }) => {
   return withAuth(async ({ organizationId }) => {
     // Only allow deleting files that belong to this org
     // Only allow deleting files that belong to this org
-    if (!fileUrl.startsWith(`/api/files/${organizationId}/`)) {
+    if (!fileUrl.startsWith(`/api/protected/files/${organizationId}/`)) {
       throw new Error("Forbidden");
       throw new Error("Forbidden");
     }
     }
 
 

+ 1 - 1
src/features/vehicles/Components/VehicleForm.tsx

@@ -116,7 +116,7 @@ export function VehicleForm({ open, onOpenChange, vehicle, customers }: VehicleF
     const formData = new FormData();
     const formData = new FormData();
     formData.append("file", compressed);
     formData.append("file", compressed);
 
 
-    const res = await fetch("/api/upload", { method: "POST", body: formData });
+    const res = await fetch("/api/protected/upload", { method: "POST", body: formData });
     if (!res.ok) {
     if (!res.ok) {
       const err = await res.json();
       const err = await res.json();
       toast.error(err.error || "Upload failed", { id: toastId });
       toast.error(err.error || "Upload failed", { id: toastId });

+ 1 - 1
src/features/vehicles/Components/service-documents-manager.tsx

@@ -99,7 +99,7 @@ export function ServiceDocumentsManager({
         formData.append("file", file);
         formData.append("file", file);
 
 
         try {
         try {
-          const res = await fetch("/api/upload/service-files", {
+          const res = await fetch("/api/protected/upload/service-files", {
             method: "POST",
             method: "POST",
             body: formData,
             body: formData,
           });
           });

+ 1 - 1
src/features/vehicles/Components/service-images-manager.tsx

@@ -76,7 +76,7 @@ export function ServiceImagesManager({
         formData.append("file", file);
         formData.append("file", file);
 
 
         try {
         try {
-          const res = await fetch("/api/upload/service-files", {
+          const res = await fetch("/api/protected/upload/service-files", {
             method: "POST",
             method: "POST",
             body: formData,
             body: formData,
           });
           });

+ 1 - 1
src/features/vehicles/Components/service-page/ServicePageClient.tsx

@@ -271,7 +271,7 @@ export function ServicePageClient({
   const handleDownloadPDF = async () => {
   const handleDownloadPDF = async () => {
     setDownloading(true)
     setDownloading(true)
     try {
     try {
-      const res = await fetch(`/api/services/${record.id}/pdf`)
+      const res = await fetch(`/api/protected/services/${record.id}/pdf`)
       if (!res.ok) throw new Error('Failed')
       if (!res.ok) throw new Error('Failed')
       const blob = await res.blob()
       const blob = await res.blob()
       const url = URL.createObjectURL(blob)
       const url = URL.createObjectURL(blob)

+ 1 - 1
src/features/vehicles/Components/service-video-manager.tsx

@@ -54,7 +54,7 @@ export function ServiceVideoManager({
         formData.append("file", file);
         formData.append("file", file);
 
 
         try {
         try {
-          const res = await fetch("/api/upload/service-files", {
+          const res = await fetch("/api/protected/upload/service-files", {
             method: "POST",
             method: "POST",
             body: formData,
             body: formData,
           });
           });

+ 1 - 0
src/lib/auth-client.ts

@@ -2,6 +2,7 @@ import { createAuthClient } from "better-auth/react";
 import { twoFactorClient } from "better-auth/plugins/two-factor";
 import { twoFactorClient } from "better-auth/plugins/two-factor";
 
 
 export const authClient = createAuthClient({
 export const authClient = createAuthClient({
+  basePath: "/api/public/auth",
   plugins: [
   plugins: [
     twoFactorClient({
     twoFactorClient({
       onTwoFactorRedirect: () => {
       onTwoFactorRedirect: () => {

+ 1 - 0
src/lib/auth.ts

@@ -9,6 +9,7 @@ const isProduction = baseURL?.startsWith("https://");
 
 
 export const auth = betterAuth({
 export const auth = betterAuth({
   baseURL,
   baseURL,
+  basePath: "/api/public/auth",
   trustedOrigins: baseURL ? [baseURL] : [],
   trustedOrigins: baseURL ? [baseURL] : [],
   database: prismaAdapter(db, {
   database: prismaAdapter(db, {
     provider: "postgresql",
     provider: "postgresql",

+ 9 - 2
src/lib/resolve-upload-path.ts

@@ -3,11 +3,18 @@ import path from "path";
 /**
 /**
  * Resolves a file URL stored in the database to an absolute file path on disk.
  * Resolves a file URL stored in the database to an absolute file path on disk.
  *
  *
- * Handles two URL formats:
- *  - New: /api/files/[orgId]/[category]/[filename] → data/uploads/[orgId]/[category]/[filename]
+ * Handles three URL formats:
+ *  - New: /api/protected/files/[orgId]/[category]/[filename] → data/uploads/[orgId]/[category]/[filename]
+ *  - Old: /api/files/[orgId]/[category]/[filename] → data/uploads/[orgId]/[category]/[filename]
  *  - Legacy: /uploads/[category]/[filename] → public/uploads/[category]/[filename]
  *  - Legacy: /uploads/[category]/[filename] → public/uploads/[category]/[filename]
  */
  */
 export function resolveUploadPath(fileUrl: string): string {
 export function resolveUploadPath(fileUrl: string): string {
+  if (fileUrl.startsWith("/api/protected/files/")) {
+    // /api/protected/files/orgId/category/filename → data/uploads/orgId/category/filename
+    const relative = fileUrl.replace("/api/protected/files/", "");
+    return path.join(process.cwd(), "data", "uploads", relative);
+  }
+
   if (fileUrl.startsWith("/api/files/")) {
   if (fileUrl.startsWith("/api/files/")) {
     // /api/files/orgId/category/filename → data/uploads/orgId/category/filename
     // /api/files/orgId/category/filename → data/uploads/orgId/category/filename
     const relative = fileUrl.replace("/api/files/", "");
     const relative = fileUrl.replace("/api/files/", "");

+ 1 - 1
src/proxy.ts

@@ -1,7 +1,7 @@
 import { NextRequest, NextResponse } from "next/server";
 import { NextRequest, NextResponse } from "next/server";
 import { getSessionCookie } from "better-auth/cookies";
 import { getSessionCookie } from "better-auth/cookies";
 
 
-const publicPaths = ["/auth", "/api/auth", "/api/health", "/api/share", "/api/public", "/api/files/public", "/api/webhooks", "/share", "/terms"];
+const publicPaths = ["/auth", "/api/v1", "/api/public", "/api/internal", "/api/webhooks", "/share", "/terms"];
 
 
 export async function proxy(request: NextRequest) {
 export async function proxy(request: NextRequest) {
   const { pathname } = request.nextUrl;
   const { pathname } = request.nextUrl;