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:
2026-10-17 20:46:15 -03:00
commit 79d12fa2f4
128 changed files with 17053 additions and 0 deletions
Executable
+76
View File
@@ -0,0 +1,76 @@
<?php
use App\Http\Controllers\Api\AiAgentController;
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\CityController;
use App\Http\Controllers\Api\LegalController;
use App\Http\Controllers\Api\StoreController;
use App\Http\Controllers\Api\UserController;
use App\Http\Middleware\EnsureUserIsActive;
use App\Http\Middleware\EnsureUserIsAdmin;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| City10 API Routes
|--------------------------------------------------------------------------
|
| Public endpoints (register, login, legal documents) are available to
| guests. All remaining routes require a Sanctum personal access token.
| Administrative resources are additionally protected by role checks.
|
*/
// ---------------------------------------------------------------------------
// Public endpoints
// ---------------------------------------------------------------------------
// The CAPTCHA lives in the session, so these public form endpoints need
// session middleware (normally reserved for the "web" group) to persist
// the challenge between the captcha and the login/register calls.
Route::middleware(StartSession::class)->group(function (): void {
Route::get('/auth/captcha', [AuthController::class, 'captcha']);
Route::post('/auth/register', [AuthController::class, 'register'])->middleware('throttle:10,1');
Route::post('/auth/login', [AuthController::class, 'login'])->middleware('throttle:10,1');
});
Route::get('/legal/documents', [LegalController::class, 'index']);
// ---------------------------------------------------------------------------
// Authenticated endpoints
// ---------------------------------------------------------------------------
Route::middleware(['auth:sanctum', EnsureUserIsActive::class])->group(function (): void {
Route::get('/auth/me', [AuthController::class, 'me']);
Route::post('/auth/logout', [AuthController::class, 'logout']);
// Legal consent (accessible before terms are accepted, by design).
Route::get('/legal/status', [LegalController::class, 'status']);
Route::post('/legal/accept', [LegalController::class, 'accept']);
Route::get('/legal/history', [LegalController::class, 'history']);
// World snapshot for the 3D renderer.
Route::get('/city/world', [CityController::class, 'world']);
// Stores: users manage their own, admins manage all.
Route::apiResource('stores', StoreController::class);
Route::get('/stores-world/seed', [StoreController::class, 'world']);
// Profile.
Route::patch('/auth/profile', [AuthController::class, 'updateProfile']);
});
// ---------------------------------------------------------------------------
// Admin-only endpoints
// ---------------------------------------------------------------------------
Route::middleware(['auth:sanctum', EnsureUserIsActive::class, EnsureUserIsAdmin::class])
->prefix('admin')
->group(function (): void {
Route::apiResource('users', UserController::class);
Route::delete('users/{user}/deactivate', [UserController::class, 'destroy']);
Route::apiResource('agents', AiAgentController::class)->except(['show']);
Route::get('/agents-meta/models', [AiAgentController::class, 'models']);
Route::get('/agents-world/seed', [AiAgentController::class, 'world']);
});
+8
View File
@@ -0,0 +1,8 @@
<?php
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
Executable
+91
View File
@@ -0,0 +1,91 @@
<?php
use App\Http\Controllers\Web\Admin\StoreAdminController;
use App\Http\Controllers\Web\AuthController;
use App\Http\Controllers\Web\ChatController;
use App\Http\Controllers\Web\CityViewerController;
use App\Http\Controllers\Web\DashboardController;
use App\Http\Controllers\Web\LanguageController;
use App\Http\Controllers\Web\LegalController;
use App\Http\Middleware\EnsureTermsAccepted;
use App\Http\Middleware\EnsureUserIsAdmin;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| City10 Web Routes
|--------------------------------------------------------------------------
|
| Public pages (welcome, legal documents, language switch) are open to
| guests. The dashboard and consent screen require an authenticated web
| session and are blocked until the current legal documents are accepted.
|
*/
// ---------------------------------------------------------------------------
// Public pages
// ---------------------------------------------------------------------------
Route::get('/', function () {
return view('welcome');
})->name('welcome');
Route::get('/lang', [LanguageController::class, 'switch'])->name('lang.switch');
// Legal documents (public, readable without an account).
Route::get('/legal/{type}', [LegalController::class, 'show'])
->whereIn('type', ['terms', 'privacy', 'cookies'])
->name('legal.show');
// ---------------------------------------------------------------------------
// Guest authentication
// ---------------------------------------------------------------------------
Route::middleware('guest')->group(function (): void {
Route::get('/login', [AuthController::class, 'showLogin'])->name('login');
Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:10,1');
Route::get('/register', [AuthController::class, 'showRegister'])->name('register');
Route::post('/register', [AuthController::class, 'register'])->middleware('throttle:10,1');
});
// ---------------------------------------------------------------------------
// Authenticated area
// ---------------------------------------------------------------------------
Route::middleware('auth')->group(function (): void {
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
// Consent screen is reachable even before terms are accepted (by design).
Route::get('/terms/accept', [LegalController::class, 'acceptForm'])->name('terms.accept');
Route::post('/terms/accept', [LegalController::class, 'accept'])->name('terms.accept.store');
Route::get('/terms/history', [LegalController::class, 'history'])->name('terms.history');
// Main platform features (blocked until terms are accepted).
Route::middleware(EnsureTermsAccepted::class)->group(function (): void {
Route::get('/dashboard', DashboardController::class)->name('dashboard');
// 3D city viewer (Phase 3).
Route::get('/city', CityViewerController::class)->name('city.viewer');
Route::get('/city/data', [CityViewerController::class, 'data'])->name('city.data');
// NPC chat (SSE stream + message POST).
Route::post('/city/chat/{agent}', [ChatController::class, 'send'])->name('city.chat.send');
Route::get('/city/chat/{agent}/stream', [ChatController::class, 'stream'])->name('city.chat.stream');
});
});
// ---------------------------------------------------------------------------
// Admin area (administrator role required, on top of auth + terms)
// ---------------------------------------------------------------------------
Route::middleware(['auth', EnsureTermsAccepted::class, EnsureUserIsAdmin::class])
->prefix('admin')
->name('admin.')
->group(function (): void {
Route::get('/stores', [StoreAdminController::class, 'index'])->name('stores.index');
Route::get('/stores/create', [StoreAdminController::class, 'create'])->name('stores.create');
Route::post('/stores', [StoreAdminController::class, 'store'])->name('stores.store');
Route::get('/stores/{store}/edit', [StoreAdminController::class, 'edit'])->name('stores.edit');
Route::put('/stores/{store}', [StoreAdminController::class, 'update'])->name('stores.update');
Route::delete('/stores/{store}', [StoreAdminController::class, 'destroy'])->name('stores.destroy');
});