Răsfoiți Sursa

improved secureity

Bernt Christian Egeland 7 luni în urmă
părinte
comite
b40565446c

+ 35 - 44
src/app/api/desktop/v1/me/route.ts

@@ -1,59 +1,36 @@
 import { NextResponse } from "next/server";
 import { db } from "@/lib/db";
+import { withDesktopAuth } from "@/lib/with-desktop-auth";
 import { rateLimit } from "@/lib/rate-limit";
 
 export async function GET(request: Request) {
-  const limited = rateLimit(request);
-  if (limited) return limited;
-
-  const authHeader = request.headers.get("Authorization");
-  if (!authHeader?.startsWith("Bearer ")) {
-    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
-  }
-  const token = authHeader.slice(7);
-
-  const session = await db.session.findFirst({
-    where: { token, expiresAt: { gt: new Date() } },
-    select: { userId: true },
-  });
-
-  if (!session) {
-    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
-  }
-
-  const user = await db.user.findUnique({
-    where: { id: session.userId },
-    select: { id: true, name: true, email: true },
-  });
+  return withDesktopAuth(request, async ({ userId, organizationId }) => {
+    const user = await db.user.findUnique({
+      where: { id: userId },
+      select: { id: true, name: true, email: true },
+    });
 
-  if (!user) {
-    return NextResponse.json({ error: "User not found" }, { status: 401 });
-  }
+    if (!user) {
+      return NextResponse.json({ error: "User not found" }, { status: 401 });
+    }
 
-  // Get active organization
-  const membership = await db.organizationMember.findFirst({
-    where: { userId: session.userId },
-    select: {
-      organizationId: true,
-      organization: { select: { id: true, name: true } },
-    },
-  });
+    const org = await db.organization.findUnique({
+      where: { id: organizationId },
+      select: { id: true, name: true },
+    });
 
-  // Get company logo
-  let companyLogo: string | null = null;
-  if (membership?.organizationId) {
+    // Get company logo
+    let companyLogo: string | null = null;
     const logoSetting = await db.appSetting.findFirst({
-      where: { organizationId: membership.organizationId, key: "workshop.logo" },
+      where: { organizationId, key: "workshop.logo" },
       select: { value: true },
     });
     companyLogo = logoSetting?.value || null;
-  }
 
-  return NextResponse.json({
-    user,
-    organization: membership?.organization
-      ? { ...membership.organization, logo: companyLogo }
-      : null,
+    return NextResponse.json({
+      user,
+      organization: org ? { ...org, logo: companyLogo } : null,
+    });
   });
 }
 
@@ -67,7 +44,21 @@ export async function DELETE(request: Request) {
   }
   const token = authHeader.slice(7);
 
-  await db.session.deleteMany({ where: { token } });
+  if (!token || token.length > 256) {
+    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+  }
+
+  // Only delete if the session is valid and belongs to a real user
+  const session = await db.session.findFirst({
+    where: { token, expiresAt: { gt: new Date() } },
+    select: { id: true },
+  });
+
+  if (!session) {
+    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+  }
+
+  await db.session.delete({ where: { id: session.id } });
 
   return new NextResponse(null, { status: 204 });
 }

+ 5 - 0
src/app/api/desktop/v1/sync/route.ts

@@ -1,6 +1,7 @@
 import { NextResponse } from "next/server";
 import { db } from "@/lib/db";
 import { withDesktopAuth } from "@/lib/with-desktop-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 
 export async function GET(request: Request) {
   return withDesktopAuth(request, async ({ organizationId }) => {
@@ -218,5 +219,9 @@ export async function GET(request: Request) {
         deletedAt: d.deletedAt,
       })),
     });
+  }, {
+    requiredPermissions: [
+      { action: PermissionAction.READ, subject: PermissionSubject.CUSTOMERS },
+    ],
   });
 }

+ 10 - 1
src/app/api/desktop/v1/work-orders/[id]/email/route.ts

@@ -10,6 +10,15 @@ import React from "react";
 import { readFile } from "fs/promises";
 import { resolveUploadPath } from "@/lib/resolve-upload-path";
 
+function escapeHtml(str: string): string {
+  return str
+    .replace(/&/g, "&")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;")
+    .replace(/"/g, "&quot;")
+    .replace(/'/g, "&#39;");
+}
+
 async function getWorkshopSettings(organizationId: string) {
   const settings = await db.appSetting.findMany({ where: { organizationId } });
   const map: Record<string, string> = {};
@@ -115,7 +124,7 @@ export async function POST(
           <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
             <h2>Invoice ${invoiceNum}</h2>
             <p>Please find your invoice attached.</p>
-            ${message ? `<p>${message}</p>` : ""}
+            ${message ? `<p>${escapeHtml(message)}</p>` : ""}
             ${publicLink ? `<p><a href="${publicLink}" style="color: #2563eb;">View Invoice Online</a></p>` : ""}
             <hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;" />
             <p style="color: #666; font-size: 14px;">

+ 26 - 7
src/lib/with-desktop-auth.ts

@@ -1,4 +1,5 @@
 import { NextResponse } from "next/server";
+import { ZodError } from "zod";
 import { db } from "./db";
 import { hasAllPermissions, type PermissionInput } from "./permissions";
 import { rateLimit } from "./rate-limit";
@@ -31,6 +32,10 @@ export async function withDesktopAuth(
   }
   const token = authHeader.slice(7);
 
+  if (!token || token.length > 256) {
+    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+  }
+
   // Look up session (expiry checked)
   const session = await db.session.findFirst({
     where: { token, expiresAt: { gt: new Date() } },
@@ -94,11 +99,25 @@ export async function withDesktopAuth(
     }
   }
 
-  return handler({
-    userId: session.userId,
-    organizationId: membership.organizationId,
-    role: isSuperAdmin ? "super_admin" : (membership.role ?? "member"),
-    isSuperAdmin,
-    isAdmin: isSuperAdmin || isOwnerOrAdmin || roleIsAdmin,
-  });
+  try {
+    return await handler({
+      userId: session.userId,
+      organizationId: membership.organizationId,
+      role: isSuperAdmin ? "super_admin" : (membership.role ?? "member"),
+      isSuperAdmin,
+      isAdmin: isSuperAdmin || isOwnerOrAdmin || roleIsAdmin,
+    });
+  } catch (err) {
+    if (err instanceof ZodError) {
+      return NextResponse.json(
+        { error: "Validation error", details: err.issues.map((e) => ({ path: e.path.join("."), message: e.message })) },
+        { status: 400 },
+      );
+    }
+    if (err instanceof SyntaxError) {
+      return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
+    }
+    console.error("[desktop-api]", err);
+    return NextResponse.json({ error: "Internal server error" }, { status: 500 });
+  }
 }