Deploy on DigitalOcean App Platform (GitHub-source build) + consolidate audit-fixes
Deploy config: - .do/app.yaml: build the Dockerfile directly from GitHub (deploy_on_push) instead of a pre-built DOCR image; NEXT_PUBLIC_* set RUN_AND_BUILD_TIME with the propertymanagement.network domain so they bake into the client bundle; add custom domains block (apex + www); wire Sentry DSN (server + browser). Included pending work from the audit-fixes branch: - AI provider abstraction (OpenAI/Anthropic, admin-selectable; Anthropic default) - Per-landlord e-signature (DocuSign OAuth + Dropbox Sign) + migration 0010 - Outbound webhooks / Zapier integration - PayPal removal (Stripe-only billing) - Storage hardening (fail-loud when Spaces unconfigured), security fixes Verified: full production Docker build (same build-args as DO) passes clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
917a06ee85
commit
5495b94924
@@ -44,7 +44,7 @@ export async function DELETE(_: Request, { params }: { params: Promise<{ id: str
|
||||
await db.delete(documents).where(and(eq(documents.id, id), eq(documents.user_id, ownerId)))
|
||||
|
||||
if (doc.storage_path) {
|
||||
await deleteFile(doc.storage_path)
|
||||
await deleteFile(doc.storage_path, ownerId)
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
|
||||
@@ -3,7 +3,14 @@ import { and, desc, eq } from "drizzle-orm"
|
||||
import { db } from "@/lib/db"
|
||||
import { documents, properties } from "@/lib/db/schema"
|
||||
import { getSessionUser } from "@/lib/session"
|
||||
import { saveFile, isAllowedUploadExt, StorageNotConfiguredError } from "@/lib/storage"
|
||||
import {
|
||||
saveFile,
|
||||
isAllowedUploadExt,
|
||||
StorageNotConfiguredError,
|
||||
keyBelongsToOwner,
|
||||
contentMatchesExtension,
|
||||
extOf,
|
||||
} from "@/lib/storage"
|
||||
import { checkStorageLimit } from "@/lib/plan-limits"
|
||||
import { ownsProperty, ownsTenant } from "@/lib/db/ownership"
|
||||
import { getEffectiveOwnerId, getAccountContext } from "@/lib/account"
|
||||
@@ -57,6 +64,10 @@ export async function POST(request: Request) {
|
||||
if (!file) return NextResponse.json({ error: "No file provided" }, { status: 400 })
|
||||
if (file.size > 20 * 1024 * 1024) return NextResponse.json({ error: "File too large (max 20 MB)" }, { status: 400 })
|
||||
if (!isAllowedUploadExt(file.name)) return NextResponse.json({ error: "File type not allowed" }, { status: 400 })
|
||||
const head = Buffer.from(await file.slice(0, 16).arrayBuffer())
|
||||
if (!contentMatchesExtension(head, extOf(file.name))) {
|
||||
return NextResponse.json({ error: "File content does not match its type" }, { status: 400 })
|
||||
}
|
||||
|
||||
const storageError = await checkStorageLimit(ownerId, file.size)
|
||||
if (storageError) return NextResponse.json({ error: storageError }, { status: 403 })
|
||||
@@ -113,6 +124,20 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: "Tenant not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
// The file reference is client-supplied. Require it to be an /api/files URL
|
||||
// inside the caller's OWN namespace, and derive storage_path from it — never
|
||||
// trust a separate client storage_path (which could point at another tenant's
|
||||
// object and later be deleted). Also blocks javascript:/external file_url values.
|
||||
const FILES_PREFIX = "/api/files/"
|
||||
const fileUrl = typeof body.file_url === "string" ? body.file_url : ""
|
||||
if (!fileUrl.startsWith(FILES_PREFIX)) {
|
||||
return NextResponse.json({ error: "file_url must reference an uploaded file" }, { status: 400 })
|
||||
}
|
||||
const storagePath = fileUrl.slice(FILES_PREFIX.length)
|
||||
if (!keyBelongsToOwner(storagePath, ownerId)) {
|
||||
return NextResponse.json({ error: "Invalid file reference" }, { status: 403 })
|
||||
}
|
||||
|
||||
// Whitelist insertable columns — never trust client-supplied user_id/id/created_at.
|
||||
const [data] = await db
|
||||
.insert(documents)
|
||||
@@ -122,8 +147,8 @@ export async function POST(request: Request) {
|
||||
tenant_id: tenantId,
|
||||
name: body.name as string,
|
||||
category: (body.category as typeof documents.$inferInsert.category) ?? "other",
|
||||
file_url: body.file_url as string,
|
||||
storage_path: body.storage_path as string | undefined,
|
||||
file_url: fileUrl,
|
||||
storage_path: storagePath,
|
||||
file_type: body.file_type as string | undefined,
|
||||
file_size: body.file_size as number | undefined,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user