Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6e374fc
feat(resources): open uploads to authenticated users with quota and m…
trtajim Oct 13, 2026
bbb04c4
feat(ui): add submissions page, public upload triggers, and navigatio…
trtajim Oct 13, 2026
a6f32fe
test(resources): add test suite for public upload and submissions
trtajim Oct 13, 2026
fbaf384
refactor: simplify resource upload modals by removing dynamic moderat…
trtajim Oct 13, 2026
6800651
feat: use unified UploadResourceDropdown with FolderLockedModal in ad…
trtajim Oct 13, 2026
ae71112
fix: hide upload resource dropdown when folder is frozen in admin Nod…
trtajim Oct 13, 2026
d669c11
refactor: use UploadResourceDropdown in public Node.vue and revert to…
trtajim Oct 13, 2026
45ea291
refactor: move UploadResourceDropdown to table header and remove empt…
trtajim Oct 13, 2026
c37305c
refactor: remove unused contributor cta card and props from EmptyState
trtajim Oct 13, 2026
d378c6b
feat(submissions): align layout with forum/blog, adopt PR 416 quota c…
trtajim Oct 13, 2026
c670eb2
Update web.php
trtajim Oct 13, 2026
858240f
refactor(admin): extract ReviewResourceModal component and clean up u…
trtajim Oct 13, 2026
195d805
refactor(admin): unify resource review modal and remove unused compon…
trtajim Oct 13, 2026
a879559
refactor: streamline review modal layout and submission cards
trtajim Oct 13, 2026
c79a689
refactor: update video playlist modal notice and simplify moderation …
trtajim Oct 13, 2026
9935b50
perf: optimize submission status counts into a single grouped query
trtajim Oct 13, 2026
1d06202
fix: enforce strict folder freeze lock without moderator bypass in st…
trtajim Oct 13, 2026
6162fea
refactor: remove redundant failedAuthorization in resource store requ…
trtajim Oct 13, 2026
1ce4526
refactor: rename ensureUnderPendingLimit to ensureCanSubmit and updat…
trtajim Oct 13, 2026
44bae2e
fix(resources): validate submission eligibility before storing files …
trtajim Oct 13, 2026
47348f3
refactor(resources): always enforce account age check in ensureCanSubmit
trtajim Oct 13, 2026
2cafcdf
test(resources): use established user in admin resource update and de…
trtajim Oct 13, 2026
c3fb7e7
feat(resources): redirect non-moderator submissions to my-submissions…
trtajim Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 9 additions & 213 deletions app/Http/Controllers/Admin/ResourceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,101 +2,41 @@

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\Resource\BulkImageStoreRequest;
use App\Http\Requests\Resource\BulkVideoStoreRequest;
use App\Http\Requests\Resource\StoreResourceRequest;
use App\Http\Controllers\ResourceController as BaseResourceController;
use App\Http\Requests\Resource\UpdateResourceRequest;
use App\Models\Node;
use App\Models\Resource;
use App\Models\ResourceChangeRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;

class ResourceController extends Controller
class ResourceController extends BaseResourceController
{
/**
* Determine maximum allowed pending submissions for the user.
* Verified users: 100, Unverified users: 30.
*/
protected function getMaxPendingSubmissions(): int
{
return Auth::user()?->is_verified ? 100 : 30;
}

/**
* Check if user would exceed their pending change requests quota.
* Throws standard ValidationException so it returns as a typed error in Inertia errors.
*/
protected function ensureUnderPendingLimit(int $incomingCount = 1): void
{
if (Auth::user()?->can('moderate resources')) {
return;
}

$userId = Auth::id();
$maxLimit = $this->getMaxPendingSubmissions();

$currentPending = ResourceChangeRequest::where('user_id', $userId)
->where('status', 'pending')
->count();

if (($currentPending + $incomingCount) > $maxLimit) {
throw ValidationException::withMessages([
'pending_limit' => "আপনি সর্বোচ্চ {$maxLimit}টি কন্টেন্ট আপলোড করার অনুরোধ করতে পারেন। আপনার আপলোডকৃত {$currentPending}টি কন্টেন্ট বর্তমানে পর্যালোচনাধীন রয়েছে, তাই অনুগ্রহ করে অপেক্ষা করুন। ",
]);
}
}

public function store(StoreResourceRequest $request)
public function update(UpdateResourceRequest $request, Resource $resource)
{
$validated = $request->validated();

if ($request->hasFile('file')) {
$validated['file_path'] = $request->file('file')->store("resources/{$validated['resource_type']}s");
}

if ($request->user()->can('moderate resources')) {
$validated['user_id'] = Auth::id();
Resource::create($validated);
$isModerator = $request->user()->can('moderate resources');

return back()->with('success', 'Resource created successfully.');
if (! $isModerator) {
$this->ensureCanSubmit(1);
}

$this->ensureUnderPendingLimit(1);

ResourceChangeRequest::recordCreate(
Auth::id(),
(int) $validated['node_id'],
$validated
);

return back()->with('success', 'Resource submitted for moderation.');
}

public function update(UpdateResourceRequest $request, Resource $resource)
{
$validated = $request->validated();

if ($request->hasFile('file')) {
if ($request->user()->can('moderate resources') && $resource->file_path) {
if ($isModerator && $resource->file_path) {
Storage::delete($resource->file_path);
}
$validated['file_path'] = $request->file('file')->store("resources/{$validated['resource_type']}s");
}

if ($request->user()->can('moderate resources')) {
if ($isModerator) {
$resource->update($validated);

return back()->with('success', 'Resource updated successfully.');
}

$this->ensureUnderPendingLimit(1);

ResourceChangeRequest::recordUpdate(
Auth::id(),
$resource,
Expand All @@ -117,157 +57,13 @@ public function destroy(Resource $resource)
return redirect()->back()->with('success', 'Resource deleted successfully.');
}

$this->ensureUnderPendingLimit(1);
$this->ensureCanSubmit(1);

ResourceChangeRequest::recordDelete(Auth::id(), $resource);

return redirect()->back()->with('success', 'Resource deletion request submitted for moderation.');
}

public function storeBulkImages(BulkImageStoreRequest $request)
{
$validated = $request->validated();
$filesCount = count($request->file('files') ?? []);
$isModerator = $request->user()->can('moderate resources');

if (! $isModerator) {
$this->ensureUnderPendingLimit($filesCount);
}

$userId = Auth::id();
$nodeId = (int) $validated['node_id'];

DB::transaction(function () use ($request, $validated, $userId, $nodeId, $isModerator) {
foreach ($request->file('files') as $index => $file) {
$filePath = $file->store('resources/images');
$title = $validated['custom_titles'][$index];

if ($isModerator) {
Resource::create([
'user_id' => $userId,
'node_id' => $nodeId,
'title' => $title,
'resource_type' => 'image',
'file_path' => $filePath,
]);
} else {
ResourceChangeRequest::recordCreate($userId, $nodeId, [
'title' => $title,
'resource_type' => 'image',
'file_path' => $filePath,
]);
}
}
});

$message = $isModerator ? 'Images uploaded successfully.' : 'Images submitted for moderation.';

return back()->with('success', $message);
}

public function storeBulkVideos(BulkVideoStoreRequest $request)
{
$validated = $request->validated();

$playlistUrl = $validated['playlist_url'];

// Extract playlist ID
parse_str(parse_url($playlistUrl, PHP_URL_QUERY), $query);

$playlistId = $query['list'] ?? null;

if (! $playlistId) {
return back()->with('error', 'Invalid Youtube URL');
}

$videos = [];
$pageToken = null;

do {
$response = Http::get('https://www.googleapis.com/youtube/v3/playlistItems', [
'part' => 'snippet',
'playlistId' => $playlistId,
'maxResults' => 50,
'pageToken' => $pageToken,
'key' => config('services.youtube.key'),
]);

if (! $response->successful()) {
return back()->with('error', 'Unable to fetch youtube url');
}

$data = $response->json();

foreach ($data['items'] as $item) {
$videos[] = [
'title' => $item['snippet']['title'],
'video_id' => $item['snippet']['resourceId']['videoId'],
'position' => $item['snippet']['position'],
];
}

$pageToken = $data['nextPageToken'] ?? null;
} while ($pageToken);

if (! empty($validated['is_reversed'])) {
$videos = array_reverse($videos);
}

// Apply naming strategy
foreach ($videos as $index => &$video) {
if ($validated['naming_strategy'] === 'youtube') {
$video['title'] = $video['title'];
} else {
$number = str_pad(
($validated['start_number'] ?? 1) + $index,
2,
'0',
STR_PAD_LEFT
);

$prefix = trim($validated['naming_prefix'] ?? '');
$video['title'] = $prefix !== '' ? "{$prefix} - {$number}" : $number;
}
}

$userId = Auth::id();
$nodeId = (int) $validated['node_id'];
$videosCount = count($videos);
$isModerator = $request->user()->can('moderate resources');

if (! $isModerator) {
$this->ensureUnderPendingLimit($videosCount);
}

DB::transaction(function () use ($videos, $userId, $nodeId, $isModerator) {
foreach ($videos as $video) {
$finalUrl = "https://www.youtube.com/watch?v={$video['video_id']}";

if ($isModerator) {
Resource::create([
'user_id' => $userId,
'node_id' => $nodeId,
'title' => $video['title'],
'resource_type' => 'video',
'external_url' => $finalUrl,
]);
} else {
ResourceChangeRequest::recordCreate($userId, $nodeId, [
'title' => $video['title'],
'resource_type' => 'video',
'external_url' => $finalUrl,
]);
}
}
});

$message = $isModerator
? 'YouTube playlist imported successfully.'
: 'YouTube playlist imported and submitted for moderation.';

return back()->with('success', $message);
}

public function bulkRename(Request $request, Node $node)
{
if ($node->isEffectivelyFrozen()) {
Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/NodeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public function show(Subject $subject, $path)
'id' => $node->id,
'name' => $node->name,
'slug' => $node->slug,
'is_frozen' => $node->isEffectivelyFrozen(),
],
'nodes' => $nodes,
'breadcrumb' => $node->breadcrumb(),
Expand Down
Loading
Loading