Commit 3df8faf6 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix(audit): resolve all runtime 500 errors found in full platform audit

- Install laravel/sanctum (fixes all 13 API route files that use auth:sanctum)
- Add HasApiTokens trait to User model
- Create missing sidebar partials (admin-sidebar, creator-sidebar) fixing 8 pages
- Add missing DB columns: last_active_at, reputation_tier (creator_profiles), admin_notes (projects)
- Fix ForceProjectStatusRequest validation to match DB CHECK constraint
- Fix Submission::revisionRequest() relationship type (belongsTo -> hasOne)
- Add eager-load for company+creator in project show controllers
- Create missing InvitationCancelled event class
- Add missing translation keys (en+ar) for sidebar navigation
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 51153dd8
...@@ -11,10 +11,11 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; ...@@ -11,10 +11,11 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable implements MustVerifyEmail class User extends Authenticatable implements MustVerifyEmail
{ {
use HasFactory, Notifiable, SoftDeletes, HasUuid; use HasApiTokens, HasFactory, Notifiable, SoftDeletes, HasUuid;
protected $fillable = [ protected $fillable = [
'uuid', 'uuid',
......
...@@ -14,7 +14,7 @@ class ForceProjectStatusRequest extends FormRequest ...@@ -14,7 +14,7 @@ class ForceProjectStatusRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
return [ return [
'status' => ['required', 'string', 'in:active,in_progress,completed,cancelled'], 'status' => ['required', 'string', 'in:not_started,in_progress,waiting_review,revision_requested,approved,completed,cancelled,disputed,on_hold'],
'reason' => ['required', 'string', 'min:10', 'max:1000'], 'reason' => ['required', 'string', 'min:10', 'max:1000'],
]; ];
} }
......
...@@ -46,14 +46,18 @@ return new class extends Migration ...@@ -46,14 +46,18 @@ return new class extends Migration
$table->boolean('is_verified')->default(false); $table->boolean('is_verified')->default(false);
$table->string('verification_level', 20)->default('email'); $table->string('verification_level', 20)->default('email');
$table->boolean('is_featured')->default(false); $table->boolean('is_featured')->default(false);
$table->string('reputation_tier', 20)->default('newcomer');
$table->string('peertube_channel_name', 100)->nullable(); $table->string('peertube_channel_name', 100)->nullable();
$table->timestamp('username_changed_at')->nullable(); $table->timestamp('username_changed_at')->nullable();
$table->timestamp('last_active_at')->nullable();
$table->timestamps(); $table->timestamps();
$table->index('country'); $table->index('country');
$table->index('availability_status'); $table->index('availability_status');
$table->index(['reputation_score']); $table->index(['reputation_score']);
$table->index('completion_percentage'); $table->index('completion_percentage');
$table->index('reputation_tier');
$table->index('last_active_at');
}); });
if (DB::connection()->getDriverName() === 'pgsql') { if (DB::connection()->getDriverName() === 'pgsql') {
......
...@@ -54,8 +54,10 @@ class CreatorProfile extends Model ...@@ -54,8 +54,10 @@ class CreatorProfile extends Model
'is_verified', 'is_verified',
'verification_level', 'verification_level',
'is_featured', 'is_featured',
'reputation_tier',
'peertube_channel_name', 'peertube_channel_name',
'username_changed_at', 'username_changed_at',
'last_active_at',
]; ];
protected function casts(): array protected function casts(): array
...@@ -75,6 +77,7 @@ class CreatorProfile extends Model ...@@ -75,6 +77,7 @@ class CreatorProfile extends Model
'is_verified' => 'boolean', 'is_verified' => 'boolean',
'is_featured' => 'boolean', 'is_featured' => 'boolean',
'username_changed_at' => 'datetime', 'username_changed_at' => 'datetime',
'last_active_at' => 'datetime',
]; ];
} }
......
...@@ -7,6 +7,7 @@ use App\Shared\Traits\HasUuid; ...@@ -7,6 +7,7 @@ use App\Shared\Traits\HasUuid;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
class Submission extends Model class Submission extends Model
{ {
...@@ -54,9 +55,9 @@ class Submission extends Model ...@@ -54,9 +55,9 @@ class Submission extends Model
return $this->hasMany(TimestampComment::class)->orderBy('timestamp_seconds'); return $this->hasMany(TimestampComment::class)->orderBy('timestamp_seconds');
} }
public function revisionRequest(): BelongsTo public function revisionRequest(): HasOne
{ {
return $this->belongsTo(RevisionRequest::class, 'id', 'submission_id'); return $this->hasOne(RevisionRequest::class, 'submission_id');
} }
public function getHasVideoAttribute(): bool public function getHasVideoAttribute(): bool
......
<?php
namespace App\Modules\Invitations\Events;
use App\Modules\Invitations\Models\Invitation;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class InvitationCancelled
{
use Dispatchable, SerializesModels;
public function __construct(
public Invitation $invitation,
) {}
}
...@@ -40,7 +40,7 @@ class CompanyProjectController extends Controller ...@@ -40,7 +40,7 @@ class CompanyProjectController extends Controller
{ {
$this->authorize('view', $project); $this->authorize('view', $project);
$project->load(['creator', 'campaign', 'activities', 'deadlineExtensions']); $project->load(['company', 'creator', 'campaign', 'activities', 'deadlineExtensions']);
if ($request->expectsJson()) { if ($request->expectsJson()) {
return new ProjectResource($project); return new ProjectResource($project);
......
...@@ -43,7 +43,7 @@ class CreatorProjectController extends Controller ...@@ -43,7 +43,7 @@ class CreatorProjectController extends Controller
{ {
$this->authorize('view', $project); $this->authorize('view', $project);
$project->load(['company', 'campaign', 'activities', 'deadlineExtensions']); $project->load(['company', 'creator', 'campaign', 'activities', 'deadlineExtensions']);
if ($request->expectsJson()) { if ($request->expectsJson()) {
return new ProjectResource($project); return new ProjectResource($project);
......
...@@ -34,6 +34,7 @@ return new class extends Migration ...@@ -34,6 +34,7 @@ return new class extends Migration
$table->text('dispute_reason')->nullable(); $table->text('dispute_reason')->nullable();
$table->timestamp('dispute_resolved_at')->nullable(); $table->timestamp('dispute_resolved_at')->nullable();
$table->text('dispute_resolution')->nullable(); $table->text('dispute_resolution')->nullable();
$table->jsonb('admin_notes')->default('[]');
$table->timestamps(); $table->timestamps();
$table->softDeletes(); $table->softDeletes();
......
...@@ -51,6 +51,7 @@ class Project extends Model ...@@ -51,6 +51,7 @@ class Project extends Model
'dispute_reason', 'dispute_reason',
'dispute_resolved_at', 'dispute_resolved_at',
'dispute_resolution', 'dispute_resolution',
'admin_notes',
]; ];
protected function casts(): array protected function casts(): array
...@@ -65,6 +66,7 @@ class Project extends Model ...@@ -65,6 +66,7 @@ class Project extends Model
'cancelled_at' => 'datetime', 'cancelled_at' => 'datetime',
'disputed_at' => 'datetime', 'disputed_at' => 'datetime',
'dispute_resolved_at' => 'datetime', 'dispute_resolved_at' => 'datetime',
'admin_notes' => 'array',
]; ];
} }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "5313b5b1fd9b2faa8917d5a58afe0839", "content-hash": "98b7325bf1969c596e93cb18d29c886d",
"packages": [ "packages": [
{ {
"name": "blade-ui-kit/blade-icons", "name": "blade-ui-kit/blade-icons",
...@@ -1557,6 +1557,69 @@ ...@@ -1557,6 +1557,69 @@
}, },
"time": "2026-06-24T19:16:38+00:00" "time": "2026-06-24T19:16:38+00:00"
}, },
{
"name": "laravel/sanctum",
"version": "v4.3.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "2a9bccc18e9907808e0018dd15fa643937886b1e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/2a9bccc18e9907808e0018dd15fa643937886b1e",
"reference": "2a9bccc18e9907808e0018dd15fa643937886b1e",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/console": "^11.0|^12.0|^13.0",
"illuminate/contracts": "^11.0|^12.0|^13.0",
"illuminate/database": "^11.0|^12.0|^13.0",
"illuminate/support": "^11.0|^12.0|^13.0",
"php": "^8.2",
"symfony/console": "^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.15|^10.8|^11.0",
"phpstan/phpstan": "^1.10"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sanctum\\SanctumServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Sanctum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
"keywords": [
"auth",
"laravel",
"sanctum"
],
"support": {
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2026-04-30T11:46:25+00:00"
},
{ {
"name": "laravel/serializable-closure", "name": "laravel/serializable-closure",
"version": "v2.0.13", "version": "v2.0.13",
......
<?php
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
use Laravel\Sanctum\Sanctum;
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
',ugcheaven.caprover.al-arcade.com',
Sanctum::currentApplicationUrlWithPort(),
))),
/*
|--------------------------------------------------------------------------
| Sanctum Guards
|--------------------------------------------------------------------------
|
| This array contains the authentication guards that will be checked when
| Sanctum is trying to authenticate a request. If none of these guards
| are able to authenticate the request, Sanctum will use the bearer
| token that's present on an incoming request for authentication.
|
*/
'guard' => ['web'],
/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. This will override any values set in the token's
| "expires_at" attribute, but first-party sessions are not affected.
|
*/
'expiration' => null,
/*
|--------------------------------------------------------------------------
| Token Prefix
|--------------------------------------------------------------------------
|
| Sanctum can prefix new tokens in order to take advantage of numerous
| security scanning initiatives maintained by open source platforms
| that notify developers if they commit tokens into repositories.
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
*/
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/
'middleware' => [
'authenticate_session' => AuthenticateSession::class,
'encrypt_cookies' => EncryptCookies::class,
'validate_csrf_token' => ValidateCsrfToken::class,
],
];
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};
...@@ -124,6 +124,10 @@ return [ ...@@ -124,6 +124,10 @@ return [
'nav_campaigns' => 'الحملات', 'nav_campaigns' => 'الحملات',
'nav_projects' => 'المشاريع', 'nav_projects' => 'المشاريع',
'nav_creators' => 'المبدعون', 'nav_creators' => 'المبدعون',
'nav_content' => 'المحتوى',
'theme_settings' => 'المظهر',
'feature_settings' => 'الميزات',
'limit_settings' => 'الحدود',
// Misc // Misc
'last_login' => 'آخر تسجيل دخول', 'last_login' => 'آخر تسجيل دخول',
......
...@@ -222,6 +222,9 @@ return [ ...@@ -222,6 +222,9 @@ return [
'verified_only' => 'الموثقون فقط', 'verified_only' => 'الموثقون فقط',
'no_creators_found_desc' => 'جرب تعديل عوامل التصفية.', 'no_creators_found_desc' => 'جرب تعديل عوامل التصفية.',
// Sidebar
'work' => 'العمل',
// Portfolio // Portfolio
'portfolio_title' => 'معرض أعمالي', 'portfolio_title' => 'معرض أعمالي',
'portfolio_subtitle' => 'اعرض أفضل أعمالك لجذب الشركات.', 'portfolio_subtitle' => 'اعرض أفضل أعمالك لجذب الشركات.',
......
...@@ -67,4 +67,10 @@ return [ ...@@ -67,4 +67,10 @@ return [
'email' => 'البريد الإلكتروني', 'email' => 'البريد الإلكتروني',
'completion' => 'الاكتمال', 'completion' => 'الاكتمال',
'profile' => 'الملف الشخصي', 'profile' => 'الملف الشخصي',
'settings' => 'الإعدادات',
'logout' => 'تسجيل الخروج',
'main_menu' => 'القائمة الرئيسية',
'dashboard' => 'لوحة التحكم',
'discover' => 'استكشاف',
'communication' => 'التواصل',
]; ];
...@@ -120,6 +120,10 @@ return [ ...@@ -120,6 +120,10 @@ return [
'nav_campaigns' => 'Campaigns', 'nav_campaigns' => 'Campaigns',
'nav_projects' => 'Projects', 'nav_projects' => 'Projects',
'nav_creators' => 'Creators', 'nav_creators' => 'Creators',
'nav_content' => 'Content',
'theme_settings' => 'Theme',
'feature_settings' => 'Features',
'limit_settings' => 'Limits',
// Settings // Settings
'settings_saved' => 'Settings saved successfully.', 'settings_saved' => 'Settings saved successfully.',
......
...@@ -222,6 +222,9 @@ return [ ...@@ -222,6 +222,9 @@ return [
'verified_only' => 'Verified Only', 'verified_only' => 'Verified Only',
'no_creators_found_desc' => 'Try adjusting your search filters.', 'no_creators_found_desc' => 'Try adjusting your search filters.',
// Sidebar
'work' => 'Work',
// Portfolio // Portfolio
'portfolio_title' => 'My Portfolio', 'portfolio_title' => 'My Portfolio',
'portfolio_subtitle' => 'Showcase your best work to attract companies.', 'portfolio_subtitle' => 'Showcase your best work to attract companies.',
......
...@@ -67,4 +67,10 @@ return [ ...@@ -67,4 +67,10 @@ return [
'email' => 'Email', 'email' => 'Email',
'completion' => 'Completion', 'completion' => 'Completion',
'profile' => 'Profile', 'profile' => 'Profile',
'settings' => 'Settings',
'logout' => 'Log Out',
'main_menu' => 'Main Menu',
'dashboard' => 'Dashboard',
'discover' => 'Discover',
'communication' => 'Communication',
]; ];
<x-ui.sidebar-group :label="__('admin.overview')">
<x-ui.sidebar-item icon="layout-dashboard" :href="route('admin.dashboard')" :active="request()->routeIs('admin.dashboard')">
{{ __('admin.nav_dashboard') }}
</x-ui.sidebar-item>
</x-ui.sidebar-group>
<x-ui.sidebar-group :label="__('admin.nav_users')">
<x-ui.sidebar-item icon="users" :href="route('admin.users.index')" :active="request()->routeIs('admin.users.*')">
{{ __('admin.nav_users') }}
</x-ui.sidebar-item>
<x-ui.sidebar-item icon="building-2" :href="route('admin.companies.pending')" :active="request()->routeIs('admin.companies.*')">
{{ __('admin.nav_pending') }}
</x-ui.sidebar-item>
<x-ui.sidebar-item icon="palette" :href="route('admin.creators.index')" :active="request()->routeIs('admin.creators.*')">
{{ __('admin.nav_creators') }}
</x-ui.sidebar-item>
</x-ui.sidebar-group>
<x-ui.sidebar-group :label="__('admin.nav_content')">
<x-ui.sidebar-item icon="megaphone" :href="route('admin.campaigns.index')" :active="request()->routeIs('admin.campaigns.*')">
{{ __('admin.nav_campaigns') }}
</x-ui.sidebar-item>
<x-ui.sidebar-item icon="folder-kanban" :href="route('admin.projects.index')" :active="request()->routeIs('admin.projects.*')">
{{ __('admin.nav_projects') }}
</x-ui.sidebar-item>
</x-ui.sidebar-group>
<x-ui.sidebar-group :label="__('ui.settings')">
<x-ui.sidebar-item icon="paintbrush" :href="route('admin.settings.branding')" :active="request()->routeIs('admin.settings.branding*')">
{{ __('admin.branding_settings') }}
</x-ui.sidebar-item>
<x-ui.sidebar-item icon="palette" :href="route('admin.settings.theme')" :active="request()->routeIs('admin.settings.theme*')">
{{ __('admin.theme_settings') }}
</x-ui.sidebar-item>
<x-ui.sidebar-item icon="toggle-left" :href="route('admin.settings.features')" :active="request()->routeIs('admin.settings.features*')">
{{ __('admin.feature_settings') }}
</x-ui.sidebar-item>
<x-ui.sidebar-item icon="sliders-horizontal" :href="route('admin.settings.limits')" :active="request()->routeIs('admin.settings.limits*')">
{{ __('admin.limit_settings') }}
</x-ui.sidebar-item>
<x-ui.sidebar-item icon="search" :href="route('admin.settings.seo')" :active="request()->routeIs('admin.settings.seo*')">
{{ __('admin.seo_settings') }}
</x-ui.sidebar-item>
<x-ui.sidebar-item icon="scroll-text" :href="route('admin.audit-log')" :active="request()->routeIs('admin.audit-log')">
{{ __('admin.nav_audit_log') }}
</x-ui.sidebar-item>
</x-ui.sidebar-group>
<x-ui.sidebar-group :label="__('ui.main_menu')">
<x-ui.sidebar-item icon="layout-dashboard" :href="route('creator.dashboard')" :active="request()->routeIs('creator.dashboard')">
{{ __('ui.dashboard') }}
</x-ui.sidebar-item>
<x-ui.sidebar-item icon="user" :href="route('creator.profile.edit')" :active="request()->routeIs('creator.profile.*')">
{{ __('ui.profile') }}
</x-ui.sidebar-item>
</x-ui.sidebar-group>
<x-ui.sidebar-group :label="__('creators.work')">
<x-ui.sidebar-item icon="briefcase" :href="route('portfolio.index')" :active="request()->routeIs('portfolio.*')">
{{ __('creators.portfolio_title') }}
</x-ui.sidebar-item>
<x-ui.sidebar-item icon="file-text" :href="route('creator.applications.index')" :active="request()->routeIs('creator.applications.*')">
{{ __('applications.my_applications') }}
</x-ui.sidebar-item>
<x-ui.sidebar-item icon="folder-kanban" :href="route('creator.projects.index')" :active="request()->routeIs('creator.projects.*')">
{{ __('projects.my_projects') }}
</x-ui.sidebar-item>
</x-ui.sidebar-group>
<x-ui.sidebar-group :label="__('ui.discover')">
<x-ui.sidebar-item icon="compass" :href="route('campaigns.discover')" :active="request()->routeIs('campaigns.discover')">
{{ __('ui.browse_campaigns') }}
</x-ui.sidebar-item>
<x-ui.sidebar-item icon="mail" :href="route('creator.invitations.index')" :active="request()->routeIs('creator.invitations.*')">
{{ __('invitations.received_invitations') }}
</x-ui.sidebar-item>
</x-ui.sidebar-group>
<x-ui.sidebar-group :label="__('ui.communication')">
<x-ui.sidebar-item icon="message-square" :href="route('messaging.index')" :active="request()->routeIs('messaging.*')">
{{ __('messaging.inbox') }}
</x-ui.sidebar-item>
<x-ui.sidebar-item icon="bell" :href="route('notifications.index')" :active="request()->routeIs('notifications.*')">
{{ __('notifications.notifications') }}
</x-ui.sidebar-item>
</x-ui.sidebar-group>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment