fix: team permissions, transactions, form sentinels and review polish
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
c4e6173e46
commit
231123f641
@@ -8,6 +8,7 @@ use App\Models\Product;
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -121,21 +122,34 @@ class ProductController extends Controller
|
||||
$validated['user_id'] = Auth::id();
|
||||
$validated['status'] = 'pending';
|
||||
|
||||
// Handle thumbnail upload
|
||||
if ($request->hasFile('thumbnail')) {
|
||||
$path = $request->file('thumbnail')->store('thumbnails', 'public');
|
||||
$validated['thumbnail'] = $path;
|
||||
$created = [];
|
||||
$obsolete = [];
|
||||
|
||||
try {
|
||||
$product = DB::transaction(function () use ($request, $validated, &$created, &$obsolete) {
|
||||
// Handle thumbnail upload
|
||||
if ($request->hasFile('thumbnail')) {
|
||||
$path = $request->file('thumbnail')->store('thumbnails', 'public');
|
||||
$created[] = $path;
|
||||
$validated['thumbnail'] = $path;
|
||||
}
|
||||
|
||||
$product = Product::create($validated);
|
||||
|
||||
// Attach tags if provided
|
||||
if ($request->has('tags')) {
|
||||
$product->tags()->attach($request->tags);
|
||||
}
|
||||
|
||||
$this->syncDetails($request, $product, true, $created, $obsolete);
|
||||
|
||||
return $product;
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
Storage::disk('public')->delete($created);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$product = Product::create($validated);
|
||||
|
||||
// Attach tags if provided
|
||||
if ($request->has('tags')) {
|
||||
$product->tags()->attach($request->tags);
|
||||
}
|
||||
|
||||
$this->syncDetails($request, $product);
|
||||
|
||||
return redirect()->route('products.show', $product)
|
||||
->with('success', 'Product submitted successfully! It will be reviewed by our moderators.');
|
||||
}
|
||||
@@ -193,23 +207,39 @@ class ProductController extends Controller
|
||||
$validated = array_diff_key($validated, Product::BADGES);
|
||||
}
|
||||
|
||||
// Handle thumbnail upload
|
||||
if ($request->hasFile('thumbnail')) {
|
||||
// Delete old thumbnail if exists
|
||||
if ($product->thumbnail) {
|
||||
Storage::disk('public')->delete($product->thumbnail);
|
||||
}
|
||||
// Only the submitter and admins manage the team
|
||||
$manageMembers = Auth::id() === $product->user_id || Auth::user()->isAdmin();
|
||||
|
||||
$path = $request->file('thumbnail')->store('thumbnails', 'public');
|
||||
$validated['thumbnail'] = $path;
|
||||
$created = [];
|
||||
$obsolete = [];
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($request, $product, $validated, $manageMembers, &$created, &$obsolete) {
|
||||
// Handle thumbnail upload
|
||||
if ($request->hasFile('thumbnail')) {
|
||||
if ($product->thumbnail) {
|
||||
$obsolete[] = $product->thumbnail;
|
||||
}
|
||||
|
||||
$path = $request->file('thumbnail')->store('thumbnails', 'public');
|
||||
$created[] = $path;
|
||||
$validated['thumbnail'] = $path;
|
||||
}
|
||||
|
||||
$product->update($validated);
|
||||
|
||||
$this->syncDetails($request, $product, $manageMembers, $created, $obsolete);
|
||||
|
||||
// Sync tags (will remove old tags and add new ones)
|
||||
$product->tags()->sync($request->tags ?? []);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
Storage::disk('public')->delete($created);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$product->update($validated);
|
||||
|
||||
$this->syncDetails($request, $product);
|
||||
|
||||
// Sync tags (will remove old tags and add new ones)
|
||||
$product->tags()->sync($request->tags ?? []);
|
||||
// Old files are only removed once the changes are committed
|
||||
Storage::disk('public')->delete($obsolete);
|
||||
|
||||
return redirect()->route('products.show', $product)
|
||||
->with('success', 'Product updated successfully!');
|
||||
@@ -231,7 +261,7 @@ class ProductController extends Controller
|
||||
'remove_screenshots.*' => 'integer',
|
||||
'plans' => 'nullable|array',
|
||||
'plans.*.name' => 'required|string|max:100',
|
||||
'plans.*.price' => 'required|numeric|min:0|max:99999',
|
||||
'plans.*.price' => 'required|numeric|min:0|max:99999|decimal:0,2',
|
||||
'plans.*.period' => ['required', Rule::in(PricingPlan::PERIODS)],
|
||||
'plans.*.description' => 'nullable|string|max:255',
|
||||
'members' => 'nullable|array',
|
||||
@@ -242,33 +272,39 @@ class ProductController extends Controller
|
||||
|
||||
/**
|
||||
* Sync related product detail data (badges are stored on the product itself).
|
||||
* Must run inside a transaction: newly stored files are added to $created,
|
||||
* files to remove after commit are added to $obsolete.
|
||||
*/
|
||||
protected function syncDetails(Request $request, Product $product): void
|
||||
protected function syncDetails(Request $request, Product $product, bool $manageMembers, array &$created, array &$obsolete): void
|
||||
{
|
||||
// The submitted plan list replaces all existing plans
|
||||
$product->pricingPlans()->delete();
|
||||
foreach (array_values((array) $request->input('plans', [])) as $i => $plan) {
|
||||
$product->pricingPlans()->create([
|
||||
'name' => $plan['name'],
|
||||
'price' => $plan['price'],
|
||||
'period' => $plan['period'],
|
||||
'description' => $plan['description'] ?? null,
|
||||
'position' => $i,
|
||||
]);
|
||||
// The submitted plan list replaces all existing plans (a missing sentinel means the form could not send one)
|
||||
if ($request->has('plans_present') || $product->wasRecentlyCreated) {
|
||||
$product->pricingPlans()->delete();
|
||||
foreach (array_values((array) $request->input('plans', [])) as $i => $plan) {
|
||||
$product->pricingPlans()->create([
|
||||
'name' => $plan['name'],
|
||||
'price' => $plan['price'],
|
||||
'period' => $plan['period'],
|
||||
'description' => $plan['description'] ?? null,
|
||||
'position' => $i,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// The submitted member list replaces all existing members
|
||||
$members = [];
|
||||
foreach ((array) $request->input('members', []) as $member) {
|
||||
$members[$member['user_id']] = ['role' => $member['role']];
|
||||
if ($manageMembers && ($request->has('members_present') || $product->wasRecentlyCreated)) {
|
||||
$members = [];
|
||||
foreach ((array) $request->input('members', []) as $member) {
|
||||
$members[$member['user_id']] = ['role' => $member['role']];
|
||||
}
|
||||
$product->members()->sync($members);
|
||||
}
|
||||
$product->members()->sync($members);
|
||||
|
||||
// Only rows belonging to this product may be removed
|
||||
$ids = array_filter((array) $request->input('remove_screenshots', []));
|
||||
if ($ids) {
|
||||
$product->screenshots()->whereIn('id', $ids)->get()->each(function ($shot) {
|
||||
Storage::disk('public')->delete($shot->path);
|
||||
$product->screenshots()->whereIn('id', $ids)->get()->each(function ($shot) use (&$obsolete) {
|
||||
$obsolete[] = $shot->path;
|
||||
$shot->delete();
|
||||
});
|
||||
}
|
||||
@@ -276,8 +312,10 @@ class ProductController extends Controller
|
||||
if ($request->hasFile('screenshots')) {
|
||||
$position = ($product->screenshots()->max('position') ?? -1) + 1;
|
||||
foreach ($request->file('screenshots') as $file) {
|
||||
$path = $file->store('screenshots', 'public');
|
||||
$created[] = $path;
|
||||
$product->screenshots()->create([
|
||||
'path' => $file->store('screenshots', 'public'),
|
||||
'path' => $path,
|
||||
'position' => $position++,
|
||||
]);
|
||||
}
|
||||
@@ -291,17 +329,16 @@ class ProductController extends Controller
|
||||
{
|
||||
Gate::authorize('delete-product', $product);
|
||||
|
||||
// Delete thumbnail if exists
|
||||
$files = $product->screenshots->pluck('path')->all();
|
||||
if ($product->thumbnail) {
|
||||
Storage::disk('public')->delete($product->thumbnail);
|
||||
}
|
||||
|
||||
foreach ($product->screenshots as $shot) {
|
||||
Storage::disk('public')->delete($shot->path);
|
||||
$files[] = $product->thumbnail;
|
||||
}
|
||||
|
||||
$product->delete();
|
||||
|
||||
// Files go only after the row is gone
|
||||
Storage::disk('public')->delete($files);
|
||||
|
||||
return redirect()->route('products.index')
|
||||
->with('success', 'Product deleted successfully!');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user