feat: add City10 platform with Laravel 13, AI agents, 3D viewer, and LGPD compliance
Initial commit of the complete City10 virtual city simulation platform. Backend (Laravel 13): - REST API with Sanctum tokens and session-based web authentication - Role-based access control (admin/user) with middleware protection - AI agent CRUD with OpenRouter.ai integration restricted to free models - Store management with 3D coordinates and AI shopkeeper assignment - Versioned legal documents (Terms, Privacy, Cookies) with LGPD audit trail - Math CAPTCHA on public forms and multi-language support (pt_BR, en, es) Frontend: - Responsive Blade templates with Tailwind CSS v4 - Three.js 3D city viewer with SSE chat streaming to AI agents - Admin panel for store and user administration Infrastructure: - MariaDB/MySQL database with comprehensive migrations - Database seeders for demo content and test accounts - PHPUnit test suite and Laravel Pint code styling - Vite build configuration with hot reload
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<?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('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedSmallInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->string('connection');
|
||||
$table->string('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
|
||||
$table->index(['connection', 'queue', 'failed_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
+33
@@ -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
|
||||
{
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Adds role-based access control columns to the users table.
|
||||
*
|
||||
* Distinguishes administrators ("admin") from regular users ("user")
|
||||
* so that API endpoints and the admin panel can be protected per role.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
// Role used for authorization: "admin" or "user".
|
||||
$table->string('role', 20)->default('user')->after('password')->index();
|
||||
|
||||
// Preferred UI language: pt_BR, en, or es.
|
||||
$table->string('locale', 5)->default('pt_BR')->after('role');
|
||||
|
||||
// Soft-delete flag so accounts can be disabled without data loss.
|
||||
$table->boolean('is_active')->default(true)->after('locale');
|
||||
|
||||
// Timestamp of the latest successful authentication (audit aid).
|
||||
$table->timestamp('last_login_at')->nullable()->after('is_active');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
$table->dropColumn(['role', 'locale', 'is_active', 'last_login_at']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Creates the "ai_agents" table.
|
||||
*
|
||||
* Each AI agent represents an autonomous NPC or manager entity inside the
|
||||
* virtual city. Decisions are delegated to models served by OpenRouter.ai,
|
||||
* restricted to an allow-list of free models configured by the administrator.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ai_agents', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
|
||||
// Human readable label, e.g. "Prefeito", "Lojista Padaria Central".
|
||||
$table->string('name', 120);
|
||||
|
||||
// Agent archetype: mayor, shopkeeper, pedestrian, traffic_officer, etc.
|
||||
$table->string('type', 40)->index();
|
||||
|
||||
// Free OpenRouter model identifier used for reasoning.
|
||||
$table->string('model', 120)->default('deepseek/deepseek-v4-flash-0731');
|
||||
|
||||
// System prompt describing the agent persona and constraints.
|
||||
$table->text('system_prompt')->nullable();
|
||||
|
||||
// Current macro goal in natural language (persisted AI state).
|
||||
$table->text('current_goal')->nullable();
|
||||
|
||||
// JSON snapshot of the latest decision payload returned by the model.
|
||||
$table->json('last_decision')->nullable();
|
||||
|
||||
// Operational state: active, paused, thinking, error.
|
||||
$table->string('status', 20)->default('active')->index();
|
||||
|
||||
// Number of consecutive failures (used for backoff/circuit breaker).
|
||||
$table->unsignedInteger('error_count')->default(0);
|
||||
|
||||
// 3D anchor position of the agent inside the city scene.
|
||||
$table->decimal('pos_x', 12, 4)->default(0);
|
||||
$table->decimal('pos_y', 12, 4)->default(0);
|
||||
$table->decimal('pos_z', 12, 4)->default(0);
|
||||
|
||||
// Rotation on the Y axis (radians) for 3D rendering.
|
||||
$table->decimal('rotation_y', 10, 6)->default(0);
|
||||
|
||||
$table->timestamp('last_think_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['type', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ai_agents');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Creates the "stores" table.
|
||||
*
|
||||
* Stores are physical shop buildings placed in the 3D city at an exact
|
||||
* (x, y, z) coordinate and are operated by an optional AI shopkeeper agent.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('stores', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
|
||||
// Store display name, e.g. "Padaria Central".
|
||||
$table->string('name', 160);
|
||||
|
||||
// Free text description shown in the city UI.
|
||||
$table->text('description')->nullable();
|
||||
|
||||
// Store category: bakery, market, hardware, restaurant, etc.
|
||||
$table->string('type', 60)->index();
|
||||
|
||||
// Owner account (regular user acting as shopkeeper).
|
||||
$table->foreignId('owner_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
|
||||
// AI shopkeeper agent driving prices and decisions (optional).
|
||||
$table->foreignId('agent_id')->nullable()->constrained('ai_agents')->nullOnDelete();
|
||||
|
||||
// 3D placement inside the virtual city scene.
|
||||
$table->decimal('pos_x', 12, 4)->default(0);
|
||||
$table->decimal('pos_y', 12, 4)->default(0);
|
||||
$table->decimal('pos_z', 12, 4)->default(0);
|
||||
|
||||
// Building footprint dimensions in world units (used by the renderer).
|
||||
$table->decimal('width', 10, 4)->default(4);
|
||||
$table->decimal('height', 10, 4)->default(4);
|
||||
$table->decimal('depth', 10, 4)->default(4);
|
||||
|
||||
// Commercial state of the store.
|
||||
$table->boolean('is_open')->default(true)->index();
|
||||
|
||||
// Current revenue in virtual currency (updated by AI/players).
|
||||
$table->decimal('revenue', 14, 2)->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['pos_x', 'pos_y', 'pos_z']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('stores');
|
||||
}
|
||||
};
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Creates legal document and consent tables.
|
||||
*
|
||||
* Stores versioned legal documents (Terms of Use, Privacy Policy and Cookie
|
||||
* Policy, all aligned with the Brazilian LGPD) together with an immutable
|
||||
* audit trail of each user consent event.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('legal_documents', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
|
||||
// Document kind: terms, privacy, cookies.
|
||||
$table->string('type', 20)->index();
|
||||
|
||||
// Version identifier, e.g. "1.0.0" (incremented on edits).
|
||||
$table->string('version', 20);
|
||||
|
||||
// Title localized per language key stored in JSON: {"pt_BR":..,"en":..,"es":..}
|
||||
$table->json('title');
|
||||
|
||||
// Full document body per language (markdown/HTML-safe text).
|
||||
$table->json('body');
|
||||
|
||||
// Whether this version is the one new users must accept.
|
||||
$table->boolean('is_current')->default(true)->index();
|
||||
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// A given type+version pair can only exist once.
|
||||
$table->unique(['type', 'version']);
|
||||
});
|
||||
|
||||
Schema::create('user_consents', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('legal_document_id')->constrained('legal_documents')->cascadeOnDelete();
|
||||
|
||||
// Moment the user accepted this specific document version.
|
||||
$table->timestamp('accepted_at');
|
||||
|
||||
// Client IP and user agent captured at acceptance (LGPD audit).
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
|
||||
// Consent channel, e.g. "web_form", "api".
|
||||
$table->string('source', 30)->default('web_form');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// One consent per user per document version (idempotent audit).
|
||||
$table->unique(['user_id', 'legal_document_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_consents');
|
||||
Schema::dropIfExists('legal_documents');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user