feat(m10): file sharing with upload, lightbox, permission, tests, e2e

- FileRepository (CRUD, soft-delete, pagination, project isolation) +
  FileStorageService (local FS save data/uploads/<projectId>/<uuid>.<ext>,
  MIME allowlist, filename sanitize, unique uuid name, member permission,
  file_shared notification to other members, file_uploaded activity log,
  SSE file.uploaded, uploader/admin delete + FS unlink)
- APIs: files list/upload(multipart), download (membership), delete
- Screens: files page + Uploader(multipart) + FileList(image Lightbox,
  PDF link, delete), ProjectNav ファイル link
- Unit tests (FileRepository, FileStorageService) + e2e file-sharing
This commit is contained in:
Ken Yasue
2026-06-25 02:02:39 +02:00
parent 1425773cd4
commit 60fef5c0c9
13 changed files with 945 additions and 0 deletions

View File

@ -0,0 +1,55 @@
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth/getCurrentUser';
import { createFileStorageService } from '@/lib/api/services';
import { UnauthorizedError, ValidationError } from '@/lib/errors';
import { handleApiError } from '@/lib/api/handleError';
export const runtime = 'nodejs';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
) {
const user = await getCurrentUser();
if (!user) return handleApiError(new UnauthorizedError());
const { projectId } = await params;
const page = Number(request.nextUrl.searchParams.get('page') ?? '1') || 1;
const service = createFileStorageService();
try {
return NextResponse.json(
service.listFiles(user.id, Number(projectId), page)
);
} catch (error) {
return handleApiError(error);
}
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
) {
const user = await getCurrentUser();
if (!user) return handleApiError(new UnauthorizedError());
const { projectId } = await params;
const formData = await request.formData();
const file = formData.get('file');
if (!(file instanceof File)) {
return handleApiError(
new ValidationError('ファイルを指定してください', 'file')
);
}
const data = Buffer.from(await file.arrayBuffer());
const service = createFileStorageService();
try {
const fileAsset = service.upload(user.id, Number(projectId), {
originalName: file.name,
mimeType: file.type || 'application/octet-stream',
data,
});
return NextResponse.json({ file: fileAsset }, { status: 201 });
} catch (error) {
return handleApiError(error);
}
}