import { NextResponse } from "next/server" import { and, desc, eq } from "drizzle-orm" import { db } from "@/lib/db" import { maintenance_requests, tenants } from "@/lib/db/schema" import { getSessionUser } from "@/lib/session" import { maintenanceSchema } from "@/lib/validations" import { ownsProperty, ownsUnit, ownsTenant } from "@/lib/db/ownership" import { logActivity } from "@/lib/activity" const VALID_STATUSES = ["open", "in_progress", "resolved", "closed"] const VALID_PRIORITIES = ["low", "medium", "high", "emergency"] export async function GET(request: Request) { const user = await getSessionUser() if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) const { searchParams } = new URL(request.url) const status = searchParams.get("status") const priority = searchParams.get("priority") const propertyId = searchParams.get("property_id") // Validate enum filters if (status && !VALID_STATUSES.includes(status)) { return NextResponse.json({ error: "Invalid status" }, { status: 400 }) } if (priority && !VALID_PRIORITIES.includes(priority)) { return NextResponse.json({ error: "Invalid priority" }, { status: 400 }) } const data = await db.query.maintenance_requests.findMany({ where: and( eq(maintenance_requests.user_id, user.id), status ? eq(maintenance_requests.status, status as typeof maintenance_requests.$inferSelect.status) : undefined, priority ? eq(maintenance_requests.priority, priority as typeof maintenance_requests.$inferSelect.priority) : undefined, propertyId ? eq(maintenance_requests.property_id, propertyId) : undefined ), with: { property: { columns: { name: true } }, unit: { columns: { unit_number: true } }, tenant: { columns: { first_name: true, last_name: true } }, }, orderBy: desc(maintenance_requests.created_at), }) return NextResponse.json(data) } export async function POST(request: Request) { const body = await request.json() const user = await getSessionUser() let userId: string if (user) { // Authenticated landlord userId = user.id } else { // Tenant portal submission — verify portal_token const portalToken = body.portal_token as string | undefined if (!portalToken) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) } const tenant = await db.query.tenants.findFirst({ where: eq(tenants.portal_token, portalToken), columns: { id: true, user_id: true, property_id: true, unit_id: true }, }) if (!tenant) return NextResponse.json({ error: "Invalid portal token" }, { status: 401 }) // Validate tenant owns the submitted property/unit — prevent cross-property submissions if (body.property_id && body.property_id !== tenant.property_id) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }) } if (body.unit_id && body.unit_id !== tenant.unit_id) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }) } if (body.tenant_id && body.tenant_id !== tenant.id) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }) } userId = tenant.user_id } const parsed = maintenanceSchema.safeParse(body) if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }) // Authenticated-landlord path: verify the caller owns the submitted references. // (The portal-token path already validates these against the tenant above.) if ( user && (!(await ownsProperty(user.id, parsed.data.property_id)) || !(await ownsUnit(user.id, parsed.data.unit_id)) || !(await ownsTenant(user.id, parsed.data.tenant_id))) ) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }) } const [data] = await db .insert(maintenance_requests) .values({ ...parsed.data, user_id: userId, status: "open" }) .returning() await logActivity({ userId, type: "maintenance_opened", title: `Maintenance request opened: ${parsed.data.title}`, description: `Priority: ${parsed.data.priority ?? "medium"}`, entityType: "maintenance", entityId: data.id, }) return NextResponse.json(data, { status: 201 }) }