Bernt Christian Egeland 7 месяцев назад
Родитель
Сommit
93d04574e9

+ 110 - 0
src/app/api/desktop/v1/customers/[id]/route.ts

@@ -0,0 +1,110 @@
+import { NextResponse } from "next/server";
+import { db } from "@/lib/db";
+import { withDesktopAuth } from "@/lib/with-desktop-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import { updateCustomerSchema } from "@/features/customers/Schema/customerSchema";
+
+export async function GET(
+  request: Request,
+  { params }: { params: Promise<{ id: string }> },
+) {
+  const { id } = await params;
+
+  return withDesktopAuth(
+    request,
+    async ({ organizationId }) => {
+      const customer = await db.customer.findFirst({
+        where: { id, organizationId },
+        include: {
+          vehicles: {
+            where: { isArchived: false },
+            include: {
+              _count: { select: { serviceRecords: true } },
+            },
+            orderBy: { updatedAt: "desc" },
+          },
+          _count: { select: { vehicles: true } },
+        },
+      });
+
+      if (!customer) {
+        return NextResponse.json({ error: "Customer not found" }, { status: 404 });
+      }
+
+      return NextResponse.json({ customer });
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.CUSTOMERS },
+      ],
+    },
+  );
+}
+
+export async function PUT(
+  request: Request,
+  { params }: { params: Promise<{ id: string }> },
+) {
+  const { id } = await params;
+
+  return withDesktopAuth(
+    request,
+    async ({ organizationId }) => {
+      const body = await request.json();
+      const { id: _schemaId, ...data } = updateCustomerSchema.parse({ ...body, id });
+
+      const result = await db.customer.updateMany({
+        where: { id, organizationId },
+        data: {
+          ...data,
+          email: data.email || null,
+          company: data.company || null,
+          phone: data.phone || null,
+          address: data.address || null,
+        },
+      });
+
+      if (result.count === 0) {
+        return NextResponse.json({ error: "Customer not found" }, { status: 404 });
+      }
+
+      const customer = await db.customer.findFirst({
+        where: { id, organizationId },
+      });
+
+      return NextResponse.json({ customer });
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.UPDATE, subject: PermissionSubject.CUSTOMERS },
+      ],
+    },
+  );
+}
+
+export async function DELETE(
+  request: Request,
+  { params }: { params: Promise<{ id: string }> },
+) {
+  const { id } = await params;
+
+  return withDesktopAuth(
+    request,
+    async ({ organizationId }) => {
+      const result = await db.customer.deleteMany({
+        where: { id, organizationId },
+      });
+
+      if (result.count === 0) {
+        return NextResponse.json({ error: "Customer not found" }, { status: 404 });
+      }
+
+      return new NextResponse(null, { status: 204 });
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.DELETE, subject: PermissionSubject.CUSTOMERS },
+      ],
+    },
+  );
+}

+ 92 - 0
src/app/api/desktop/v1/customers/route.ts

@@ -0,0 +1,92 @@
+import { NextResponse } from "next/server";
+import { db } from "@/lib/db";
+import { withDesktopAuth } from "@/lib/with-desktop-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import { createCustomerSchema } from "@/features/customers/Schema/customerSchema";
+import { getFeatures, FeatureGatedError } from "@/lib/features";
+
+export async function GET(request: Request) {
+  return withDesktopAuth(
+    request,
+    async ({ organizationId }) => {
+      const url = new URL(request.url);
+      const page = Math.max(1, parseInt(url.searchParams.get("page") || "1", 10));
+      const pageSize = Math.min(100, Math.max(1, parseInt(url.searchParams.get("pageSize") || "20", 10)));
+      const search = url.searchParams.get("search") || "";
+      const skip = (page - 1) * pageSize;
+
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const where: any = { organizationId };
+
+      if (search) {
+        where.OR = [
+          { name: { contains: search, mode: "insensitive" } },
+          { email: { contains: search, mode: "insensitive" } },
+          { phone: { contains: search, mode: "insensitive" } },
+          { company: { contains: search, mode: "insensitive" } },
+        ];
+      }
+
+      const [customers, total] = await Promise.all([
+        db.customer.findMany({
+          where,
+          include: {
+            _count: { select: { vehicles: true } },
+          },
+          orderBy: { updatedAt: "desc" },
+          skip,
+          take: pageSize,
+        }),
+        db.customer.count({ where }),
+      ]);
+
+      return NextResponse.json({
+        customers,
+        total,
+        page,
+        pageSize,
+        totalPages: Math.ceil(total / pageSize),
+      });
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.CUSTOMERS },
+      ],
+    },
+  );
+}
+
+export async function POST(request: Request) {
+  return withDesktopAuth(
+    request,
+    async ({ userId, organizationId }) => {
+      const body = await request.json();
+      const data = createCustomerSchema.parse(body);
+
+      const features = await getFeatures(organizationId);
+      const count = await db.customer.count({ where: { organizationId } });
+      if (count >= features.maxCustomers) {
+        throw new FeatureGatedError(
+          "maxCustomers",
+          "Customer limit reached. Upgrade your plan to add more customers.",
+        );
+      }
+
+      const customer = await db.customer.create({
+        data: {
+          ...data,
+          email: data.email || null,
+          userId,
+          organizationId,
+        },
+      });
+
+      return NextResponse.json({ customer }, { status: 201 });
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.CREATE, subject: PermissionSubject.CUSTOMERS },
+      ],
+    },
+  );
+}

+ 104 - 0
src/lib/with-desktop-auth.ts

@@ -0,0 +1,104 @@
+import { NextResponse } from "next/server";
+import { db } from "./db";
+import { hasAllPermissions, type PermissionInput } from "./permissions";
+import { rateLimit } from "./rate-limit";
+
+export type DesktopAuthContext = {
+  userId: string;
+  organizationId: string;
+  role: string;
+  isSuperAdmin: boolean;
+  isAdmin: boolean;
+};
+
+type WithDesktopAuthOptions = {
+  requiredPermissions?: PermissionInput[];
+};
+
+export async function withDesktopAuth(
+  request: Request,
+  handler: (ctx: DesktopAuthContext) => Promise<NextResponse>,
+  options: WithDesktopAuthOptions = {},
+): Promise<NextResponse> {
+  // Rate limit
+  const rateLimitResult = rateLimit(request);
+  if (rateLimitResult) return rateLimitResult;
+
+  // Extract Bearer token
+  const authHeader = request.headers.get("Authorization");
+  if (!authHeader?.startsWith("Bearer ")) {
+    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+  }
+  const token = authHeader.slice(7);
+
+  // Look up session (expiry checked)
+  const session = await db.session.findFirst({
+    where: { token, expiresAt: { gt: new Date() } },
+    select: { userId: true },
+  });
+
+  if (!session) {
+    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+  }
+
+  // Check super admin status
+  const user = await db.user.findUnique({
+    where: { id: session.userId },
+    select: { isSuperAdmin: true },
+  });
+
+  const isSuperAdmin = user?.isSuperAdmin ?? false;
+
+  // Get org membership (same pattern as getCachedMembership but using header)
+  const activeOrgId = request.headers.get("X-Org-Id");
+
+  const memberSelect = {
+    organizationId: true,
+    role: true,
+    roleId: true,
+    customRole: {
+      select: { isAdmin: true, permissions: { select: { action: true, subject: true } } },
+    },
+  } as const;
+
+  let membership;
+  if (activeOrgId) {
+    membership = await db.organizationMember.findFirst({
+      where: { userId: session.userId, organizationId: activeOrgId },
+      select: memberSelect,
+    });
+  }
+  if (!membership) {
+    membership = await db.organizationMember.findFirst({
+      where: { userId: session.userId },
+      select: memberSelect,
+    });
+  }
+
+  if (!membership?.organizationId) {
+    return NextResponse.json({ error: "No organization found" }, { status: 403 });
+  }
+
+  const isOwnerOrAdmin = membership.role === "owner" || membership.role === "admin";
+  const roleIsAdmin = membership.customRole?.isAdmin === true;
+
+  // Check permissions (super admins bypass all permission checks)
+  if (!isSuperAdmin && options.requiredPermissions && options.requiredPermissions.length > 0) {
+    const hasNoCustomRole = !membership.roleId;
+
+    if (!isOwnerOrAdmin && !roleIsAdmin && !hasNoCustomRole) {
+      const userPermissions = membership.customRole?.permissions ?? [];
+      if (!hasAllPermissions(userPermissions, options.requiredPermissions)) {
+        return NextResponse.json({ error: "Insufficient permissions" }, { status: 403 });
+      }
+    }
+  }
+
+  return handler({
+    userId: session.userId,
+    organizationId: membership.organizationId,
+    role: isSuperAdmin ? "super_admin" : (membership.role ?? "member"),
+    isSuperAdmin,
+    isAdmin: isSuperAdmin || isOwnerOrAdmin || roleIsAdmin,
+  });
+}