Eliminate all code, tests, and utilities related to the legacy 'unclaimed' user scope. All API endpoints, document and audiobook storage, and access logic now require a valid authenticated userId and only operate on resources owned by that user. Remove related helper functions, test cases, and conditional flows for anonymous/unclaimed data. Update types, client APIs, and UI logic to reflect that only 'user' scope is supported. This simplifies ownership checks and removes ambiguity around document and audiobook access.
100 lines
3.5 KiB
TypeScript
100 lines
3.5 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { and, eq, inArray } from 'drizzle-orm';
|
|
import { db } from '@/db';
|
|
import { documents } from '@/db/schema';
|
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
|
import { contentTypeForName } from '@/lib/server/storage/library-mount';
|
|
import { getDocumentBlob, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents/blobstore';
|
|
import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace';
|
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
|
import { errorToLog, serverLogger } from '@/lib/server/logger';
|
|
import { errorResponse } from '@/lib/server/errors/next-response';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
function s3NotConfiguredResponse(): NextResponse {
|
|
return NextResponse.json(
|
|
{ error: 'Documents storage is not configured. Set S3_* environment variables.' },
|
|
{ status: 503 },
|
|
);
|
|
}
|
|
|
|
function streamBuffer(buffer: Buffer): ReadableStream<Uint8Array> {
|
|
return new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(new Uint8Array(buffer));
|
|
controller.close();
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function GET(req: NextRequest) {
|
|
try {
|
|
if (!isS3Configured()) return s3NotConfiguredResponse();
|
|
|
|
const ctxOrRes = await requireAuthContext(req);
|
|
if (ctxOrRes instanceof Response) return ctxOrRes;
|
|
if (!ctxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
|
|
const testNamespace = getOpenReaderTestNamespace(req.headers);
|
|
const storageUserId = ctxOrRes.userId;
|
|
const allowedUserIds = [storageUserId];
|
|
|
|
const url = new URL(req.url);
|
|
const id = (url.searchParams.get('id') || '').trim().toLowerCase();
|
|
if (!isValidDocumentId(id)) {
|
|
return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
|
|
}
|
|
|
|
serverLogger.info({
|
|
event: 'documents.blob.get.fallback.proxy_used',
|
|
degraded: true,
|
|
fallbackPath: 'download_proxy',
|
|
documentId: id,
|
|
}, 'Document download fallback proxy used');
|
|
|
|
const rows = (await db
|
|
.select({ id: documents.id, userId: documents.userId, name: documents.name })
|
|
.from(documents)
|
|
.where(and(eq(documents.id, id), inArray(documents.userId, allowedUserIds)))) as Array<{
|
|
id: string;
|
|
userId: string;
|
|
name: string;
|
|
}>;
|
|
|
|
const doc = rows.find((row) => row.userId === storageUserId) ?? rows[0];
|
|
if (!doc) {
|
|
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
}
|
|
|
|
const filename = doc.name || `${id}.bin`;
|
|
const responseType = contentTypeForName(filename);
|
|
|
|
try {
|
|
const content = await getDocumentBlob(id, testNamespace);
|
|
return new NextResponse(streamBuffer(content), {
|
|
headers: {
|
|
'Content-Type': responseType,
|
|
'Cache-Control': 'no-store',
|
|
},
|
|
});
|
|
} catch (error) {
|
|
if (isMissingBlobError(error)) {
|
|
await db
|
|
.delete(documents)
|
|
.where(and(eq(documents.id, id), inArray(documents.userId, allowedUserIds)));
|
|
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
}
|
|
throw error;
|
|
}
|
|
} catch (error) {
|
|
serverLogger.error({
|
|
event: 'documents.blob.get.fallback.failed',
|
|
error: errorToLog(error),
|
|
}, 'Failed to load document content fallback');
|
|
return errorResponse(error, {
|
|
apiErrorMessage: 'Failed to load document content',
|
|
normalize: { code: 'DOCUMENTS_BLOB_GET_FALLBACK_FAILED', errorClass: 'storage' },
|
|
});
|
|
}
|
|
}
|