diff --git a/laravel-app/app/Http/Controllers/BookController.php b/laravel-app/app/Http/Controllers/BookController.php index 093d108..bf0de31 100644 --- a/laravel-app/app/Http/Controllers/BookController.php +++ b/laravel-app/app/Http/Controllers/BookController.php @@ -54,31 +54,35 @@ public function store(Request $request) 'thumbnail_url' => 'nullable|url', ]); - // 同じISBNの最大copy_numberを取得して+1 - $maxCopy = Book::where('isbn', $request->isbn)->max('copy_number') ?? 0; - $copyNumber = $maxCopy + 1; - - // データベースに保存 - $book = Book::create([ - 'title' => $request->title, - 'author' => $request->author, - 'isbn' => $request->isbn, - 'copy_number' => $copyNumber, - 'publisher' => $request->publisher, - 'published_date' => $request->published_date, - 'description' => $request->description, - 'thumbnail_url' => $request->thumbnail_url, - ]); + return \Illuminate\Support\Facades\DB::transaction(function () use ($request) { + // 同じISBNの最大copy_numberを取得して+1(排他ロック) + $maxCopy = Book::where('isbn', $request->isbn) + ->lockForUpdate() + ->max('copy_number') ?? 0; + $copyNumber = $maxCopy + 1; + + // データベースに保存 + $book = Book::create([ + 'title' => $request->title, + 'author' => $request->author, + 'isbn' => $request->isbn, + 'copy_number' => $copyNumber, + 'publisher' => $request->publisher, + 'published_date' => $request->published_date, + 'description' => $request->description, + 'thumbnail_url' => $request->thumbnail_url, + ]); - $copyLabel = $copyNumber > 1 ? "(冊{$copyNumber})" : ''; - Log::info("Book registered: {$book->title}{$copyLabel} (ID: {$book->id})"); - AuditLog::log('book_created', $book, "{$book->title}{$copyLabel}"); + $copyLabel = $copyNumber > 1 ? "(冊{$copyNumber})" : ''; + Log::info("Book registered: {$book->title}{$copyLabel} (ID: {$book->id})"); + AuditLog::log('book_created', $book, "{$book->title}{$copyLabel}"); - $message = $copyNumber > 1 - ? "書籍を登録しました({$copyNumber}冊目)" - : '書籍を登録しました'; + $message = $copyNumber > 1 + ? "書籍を登録しました({$copyNumber}冊目)" + : '書籍を登録しました'; - return redirect()->route('books.index')->with('success', $message); + return redirect()->route('books.index')->with('success', $message); + }); } // ISBN検索(拡張版) diff --git a/laravel-app/app/Http/Controllers/LoanController.php b/laravel-app/app/Http/Controllers/LoanController.php index 6138071..ccd9105 100644 --- a/laravel-app/app/Http/Controllers/LoanController.php +++ b/laravel-app/app/Http/Controllers/LoanController.php @@ -75,6 +75,11 @@ public function returnBook(Loan $loan) ->with('error', '他人の貸出記録は操作できません'); } + // 既に返却済みかチェック + if ($loan->returned_at) { + return redirect()->route('loans.my')->with('error', 'この貸出は既に返却済みです'); + } + // 返却処理 $loan->update([ 'returned_at' => now(), diff --git a/laravel-app/app/Http/Controllers/ProfileController.php b/laravel-app/app/Http/Controllers/ProfileController.php index a48eb8d..bf8eaca 100644 --- a/laravel-app/app/Http/Controllers/ProfileController.php +++ b/laravel-app/app/Http/Controllers/ProfileController.php @@ -46,6 +46,13 @@ public function destroy(Request $request): RedirectResponse 'password' => ['required', 'current_password'], ]); + // 貸出中の本があれば削除を拒否 + $activeLoanCount = $request->user()->loans() + ->whereNull('returned_at')->count(); + if ($activeLoanCount > 0) { + return back()->with('error', '貸出中の本があるため、アカウントを削除できません。先に返却してください。'); + } + $user = $request->user(); Auth::logout(); diff --git a/laravel-app/app/Http/Middleware/SecurityHeadersMiddleware.php b/laravel-app/app/Http/Middleware/SecurityHeadersMiddleware.php index 3521e37..7966a83 100644 --- a/laravel-app/app/Http/Middleware/SecurityHeadersMiddleware.php +++ b/laravel-app/app/Http/Middleware/SecurityHeadersMiddleware.php @@ -18,6 +18,10 @@ public function handle(Request $request, Closure $next): Response $response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade'); $response->headers->set('Content-Security-Policy', "default-src 'self' http: https: data: blob: 'unsafe-inline' 'unsafe-eval'"); + if (app()->environment('production')) { + $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + } + return $response; } } diff --git a/laravel-app/routes/api.php b/laravel-app/routes/api.php index 4dcf550..d94923d 100644 --- a/laravel-app/routes/api.php +++ b/laravel-app/routes/api.php @@ -19,8 +19,8 @@ return $request->user(); }); -// ISBN検索API -Route::get('/book/info/{isbn}', function (Request $request, $isbn) { +// ISBN検索API(1分あたり30回まで) +Route::middleware('throttle:30,1')->get('/book/info/{isbn}', function (Request $request, $isbn) { try { $service = app(BookSearchService::class); $bookData = $service->fetchByIsbn($isbn);