Add NAS file manager integration and all pending platform changes

- Installed p7h/nas-file-manager package via private VCS repo
- Published config/nas-file-manager.php with super_admin middleware restriction
- Added NAS env vars to .env.example
- Created admin/nas-storage page with connection info panel and file browser widget
- Added NAS Storage link to admin sidebar (super_admin only)
- Added SuperAdminController@nasStorage method and admin.nas-storage route
- Includes all accumulated branch changes: profile wall, 2FA, audit logs,
  settings panel, country/phone/timezone components, posts, slideshow,
  playlist shares, video downloads/shares, comment likes, notifications,
  social links, and more

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
ghassanandClaude Sonnet 4.6 committed 2026-05-13 13:24:32 +03:00
1 parent d44490dfe0
commit 0b2e95ea65
161 files changed
+27496 -7250

No files matched your search

@@ -0,0 +1,30 @@
<?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::table('video_views', function (Blueprint $table) {
$table->string('ip_address', 45)->nullable()->after('video_id');
$table->string('country', 2)->nullable()->after('ip_address');
$table->string('country_name', 100)->nullable()->after('country');
// Allow null so guest (unauthenticated) views can be recorded
$table->unsignedBigInteger('user_id')->nullable()->change();
});
}
public function down(): void
{
Schema::table('video_views', function (Blueprint $table) {
$table->dropColumn(['ip_address', 'country', 'country_name']);
$table->unsignedBigInteger('user_id')->nullable(false)->change();
});
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('gender', 20)->nullable()->after('birthday');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('gender');
});
}
};
@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('nationality', 2)->nullable()->after('gender'); // ISO2 e.g. "BH"
$table->string('phone_code', 20)->nullable()->after('nationality'); // e.g. "+973|BH"
$table->string('phone_number', 30)->nullable()->after('phone_code');
$table->string('timezone', 60)->nullable()->after('phone_number'); // IANA e.g. "Asia/Bahrain"
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['nationality', 'phone_code', 'phone_number', 'timezone']);
});
}
};
@@ -0,0 +1,28 @@
<?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::table('users', function (Blueprint $table) {
$table->string('whatsapp', 30)->nullable()->after('tiktok');
$table->string('google_location', 500)->nullable()->after('whatsapp');
$table->string('social_phone', 30)->nullable()->after('google_location');
$table->string('social_email', 100)->nullable()->after('social_phone');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['whatsapp', 'google_location', 'social_phone', 'social_email']);
});
}
};
@@ -0,0 +1,52 @@
<?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('user_social_links', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('platform', 30);
$table->string('value', 500);
$table->unsignedSmallInteger('sort_order')->default(0);
$table->timestamps();
$table->index(['user_id', 'sort_order']);
});
// Migrate existing individual social columns into the new table
$cols = ['twitter','instagram','facebook','youtube','linkedin','tiktok',
'website','whatsapp','google_location','social_phone','social_email'];
foreach (\DB::table('users')->get() as $user) {
$order = 0;
foreach ($cols as $col) {
$val = $user->$col ?? null;
if ($val) {
\DB::table('user_social_links')->insert([
'user_id' => $user->id,
'platform' => $col,
'value' => $val,
'sort_order' => $order++,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_social_links');
}
};
@@ -0,0 +1,32 @@
<?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::table('user_social_links', function (Blueprint $table) {
$table->enum('visibility', ['public', 'registered', 'subscribers', 'only_me'])
->default('public')
->after('value');
});
// Default sensitive platforms to 'subscribers'
\DB::table('user_social_links')
->whereIn('platform', ['whatsapp', 'social_phone', 'social_email'])
->update(['visibility' => 'subscribers']);
}
public function down(): void
{
Schema::table('user_social_links', function (Blueprint $table) {
$table->dropColumn('visibility');
});
}
};
@@ -0,0 +1,25 @@
<?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::table('videos', function (Blueprint $table) {
$table->boolean('allow_download')->default(false)->after('is_shorts');
});
}
public function down(): void
{
Schema::table('videos', function (Blueprint $table) {
$table->dropColumn('allow_download');
});
}
};
@@ -0,0 +1,30 @@
<?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('user_subscriptions', function (Blueprint $table) {
$table->id();
$table->foreignId('subscriber_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('channel_id')->constrained('users')->cascadeOnDelete();
$table->timestamp('created_at')->useCurrent();
$table->unique(['subscriber_id', 'channel_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_subscriptions');
}
};
@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('videos', function (Blueprint $table) {
$table->string('download_access')->default('disabled')->after('allow_download');
});
// Migrate existing data
DB::table('videos')->where('allow_download', true)->update(['download_access' => 'everyone']);
Schema::table('videos', function (Blueprint $table) {
$table->dropColumn('allow_download');
});
}
public function down(): void
{
Schema::table('videos', function (Blueprint $table) {
$table->boolean('allow_download')->default(false)->after('download_access');
});
DB::table('videos')->where('download_access', 'everyone')->update(['allow_download' => true]);
Schema::table('videos', function (Blueprint $table) {
$table->dropColumn('download_access');
});
}
};
@@ -0,0 +1,31 @@
<?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('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('notifications');
}
};
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->text('body')->nullable();
$table->foreignId('video_id')->nullable()->constrained()->nullOnDelete();
$table->string('image')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('post_reactions', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->string('type', 20)->default('like');
$table->timestamps();
$table->unique(['user_id', 'post_id']);
});
}
public function down(): void
{
Schema::dropIfExists('post_reactions');
}
};
@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('videos', function (Blueprint $table) {
$table->unsignedInteger('download_count')->default(0)->after('has_hls');
$table->unsignedInteger('share_count')->default(0)->after('download_count');
});
}
public function down(): void
{
Schema::table('videos', function (Blueprint $table) {
$table->dropColumn(['download_count', 'share_count']);
});
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('video_downloads', function (Blueprint $table) {
$table->id();
$table->foreignId('video_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null');
$table->string('ip_address', 45)->nullable();
$table->string('country', 2)->nullable();
$table->string('country_name', 100)->nullable();
$table->string('type', 10)->default('video'); // 'video' or 'mp3'
$table->timestamp('downloaded_at')->useCurrent();
$table->index(['video_id', 'downloaded_at']);
$table->index(['video_id', 'user_id']);
});
}
public function down(): void
{
Schema::dropIfExists('video_downloads');
}
};
@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('video_shares', function (Blueprint $table) {
$table->id();
$table->foreignId('video_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('token', 12)->unique();
$table->timestamp('created_at')->useCurrent();
});
Schema::create('share_accesses', function (Blueprint $table) {
$table->id();
$table->foreignId('share_id')->constrained('video_shares')->cascadeOnDelete();
$table->string('device_id', 64);
$table->string('ip_address', 45)->nullable();
$table->string('country', 2)->nullable();
$table->string('country_name')->nullable();
$table->timestamp('accessed_at')->useCurrent();
$table->unique(['share_id', 'device_id']);
});
}
public function down(): void
{
Schema::dropIfExists('share_accesses');
Schema::dropIfExists('video_shares');
}
};
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('comment_likes', function (Blueprint $table) {
$table->id();
$table->foreignId('comment_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamp('created_at')->nullable();
$table->unique(['comment_id', 'user_id']);
});
}
public function down(): void
{
Schema::dropIfExists('comment_likes');
}
};
@@ -0,0 +1,26 @@
<?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::table('users', function (Blueprint $table) {
$table->string('two_factor_secret')->nullable()->after('password');
$table->boolean('two_factor_enabled')->default(false)->after('two_factor_secret');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['two_factor_secret', 'two_factor_enabled']);
});
}
};
@@ -0,0 +1,34 @@
<?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('video_slides', function (Blueprint $table) {
$table->id();
$table->foreignId('video_id')->constrained()->cascadeOnDelete();
$table->string('filename');
$table->unsignedSmallInteger('position')->default(0);
$table->timestamps();
});
Schema::table('videos', function (Blueprint $table) {
$table->string('slideshow_video_path')->nullable()->after('thumbnail');
});
}
public function down(): void
{
Schema::dropIfExists('video_slides');
Schema::table('videos', function (Blueprint $table) {
$table->dropColumn('slideshow_video_path');
});
}
};
@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
Schema::create('settings', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->text('value')->nullable();
$table->timestamps();
});
// Seed defaults
$defaults = [
['key' => 'gpu_enabled', 'value' => 'true'],
['key' => 'gpu_device', 'value' => '0'],
['key' => 'gpu_encoder', 'value' => 'h264_nvenc'],
['key' => 'gpu_hwaccel', 'value' => 'cuda'],
['key' => 'gpu_preset', 'value' => 'p4'],
];
foreach ($defaults as $row) {
DB::table('settings')->insert(array_merge($row, [
'created_at' => now(), 'updated_at' => now(),
]));
}
}
public function down(): void
{
Schema::dropIfExists('settings');
}
};
@@ -0,0 +1,61 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (DB::getDriverName() === 'mysql') {
DB::statement("ALTER TABLE playlists MODIFY COLUMN visibility ENUM('public', 'private', 'unlisted') NOT NULL DEFAULT 'private'");
} else {
// SQLite stores enums as CHECK constraints; use table rebuild to update the constraint.
$this->rebuildSqliteEnum(['public', 'private', 'unlisted']);
}
}
public function down(): void
{
if (DB::getDriverName() === 'mysql') {
DB::table('playlists')->where('visibility', 'unlisted')->update(['visibility' => 'private']);
DB::statement("ALTER TABLE playlists MODIFY COLUMN visibility ENUM('public', 'private') NOT NULL DEFAULT 'private'");
} else {
DB::table('playlists')->where('visibility', 'unlisted')->update(['visibility' => 'private']);
$this->rebuildSqliteEnum(['public', 'private']);
}
}
private function rebuildSqliteEnum(array $values): void
{
$inList = implode(', ', array_map(fn($v) => "'$v'", $values));
DB::statement('PRAGMA foreign_keys = OFF');
DB::statement("
CREATE TABLE playlists_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
user_id INTEGER NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
thumbnail VARCHAR(255),
visibility VARCHAR(255) CHECK (visibility IN ({$inList})) NOT NULL DEFAULT 'private',
is_default TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME,
updated_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
");
DB::statement('INSERT INTO playlists_new SELECT * FROM playlists');
DB::statement('DROP TABLE playlists');
DB::statement('ALTER TABLE playlists_new RENAME TO playlists');
// Recreate indexes
DB::statement('CREATE INDEX playlists_user_id_visibility_index ON playlists (user_id, visibility)');
DB::statement('PRAGMA foreign_keys = ON');
}
};
@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use App\Models\Playlist;
return new class extends Migration
{
public function up(): void
{
Schema::table('playlists', function (Blueprint $table) {
$table->string('share_token', 64)->nullable()->unique()->after('is_default');
});
// Backfill tokens for existing playlists
Playlist::whereNull('share_token')->each(function ($playlist) {
$playlist->updateQuietly(['share_token' => Str::random(32)]);
});
}
public function down(): void
{
Schema::table('playlists', function (Blueprint $table) {
$table->dropColumn('share_token');
});
}
};
@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('playlist_shares', function (Blueprint $table) {
$table->id();
$table->foreignId('playlist_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('token', 12)->unique();
$table->timestamp('created_at')->useCurrent();
});
Schema::create('playlist_share_accesses', function (Blueprint $table) {
$table->id();
$table->foreignId('share_id')->constrained('playlist_shares')->cascadeOnDelete();
$table->string('device_id', 64);
$table->string('ip_address', 45)->nullable();
$table->string('country', 2)->nullable();
$table->string('country_name')->nullable();
$table->timestamp('accessed_at')->useCurrent();
$table->unique(['share_id', 'device_id']);
});
}
public function down(): void
{
Schema::dropIfExists('playlist_share_accesses');
Schema::dropIfExists('playlist_shares');
}
};
@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use App\Models\Video;
return new class extends Migration
{
public function up(): void
{
Schema::table('videos', function (Blueprint $table) {
$table->string('share_token', 64)->nullable()->unique()->after('share_count');
});
// Backfill existing videos
Video::whereNull('share_token')->each(function ($video) {
$video->updateQuietly(['share_token' => Str::random(32)]);
});
}
public function down(): void
{
Schema::table('videos', function (Blueprint $table) {
$table->dropColumn('share_token');
});
}
};
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('post_images', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->string('filename');
$table->unsignedTinyInteger('sort_order')->default(0);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('post_images');
}
};
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('post_videos', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->foreignId('video_id')->nullable()->nullOnDelete();
$table->unsignedTinyInteger('sort_order')->default(0);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('post_videos');
}
};
@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('username', 50)->nullable()->unique()->after('name');
});
// Back-fill existing users with a unique slug
DB::table('users')->orderBy('id')->each(function ($user) {
$base = substr(Str::slug(trim($user->name)), 0, 18);
if ($base === '') $base = 'user';
do {
$slug = $base . '-' . Str::lower(Str::random(6));
} while (DB::table('users')->where('username', $slug)->exists());
DB::table('users')->where('id', $user->id)->update(['username' => $slug]);
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('username');
});
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('banner')->nullable()->after('avatar');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('banner');
});
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('audit_logs', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id')->nullable();
$table->string('user_name')->nullable();
$table->string('action', 64);
$table->string('subject_type', 64)->nullable();
$table->string('subject_id', 64)->nullable();
$table->string('subject_label')->nullable();
$table->string('ip_address', 45)->nullable();
$table->string('user_agent')->nullable();
$table->json('details')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->index('user_id');
$table->index('action');
$table->index('created_at');
$table->index('ip_address');
});
}
public function down(): void
{
Schema::dropIfExists('audit_logs');
}
};