From c34269d0e9f0e9b34836b6d7f00cb008b96d851e Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 15 Aug 2026 23:47:03 +0200 Subject: [PATCH] 53 - Collapse the fake user layer --- .env.example | 3 +- .../Auth/RegisteredUserController.php | 60 --------- app/Http/Controllers/CounterController.php | 6 +- app/Http/Controllers/TrackerController.php | 12 +- app/Http/Middleware/HandleInertiaRequests.php | 3 - app/Http/Requests/Auth/LoginRequest.php | 86 ------------- .../Settings/ProfileUpdateRequest.php | 32 ----- app/Models/Tracker.php | 9 +- app/Models/User.php | 52 -------- config/auth.php | 117 ------------------ config/session.php | 2 +- database/factories/TrackerFactory.php | 2 - database/factories/UserFactory.php | 45 ------- ...6_08_15_000003_drop_assets_and_pricing.php | 2 +- ...6_08_15_000004_drop_users_and_sessions.php | 57 +++++++++ database/seeders/DatabaseSeeder.php | 12 +- routes/auth.php | 8 -- routes/web.php | 2 - tests/Feature/CountBackfillMigrationTest.php | 9 -- tests/Feature/DropAssetsMigrationTest.php | 9 -- tests/Feature/DropUsersMigrationTest.php | 86 +++++++++++++ 21 files changed, 161 insertions(+), 453 deletions(-) delete mode 100644 app/Http/Controllers/Auth/RegisteredUserController.php delete mode 100644 app/Http/Requests/Auth/LoginRequest.php delete mode 100644 app/Http/Requests/Settings/ProfileUpdateRequest.php delete mode 100644 app/Models/User.php delete mode 100644 config/auth.php delete mode 100644 database/factories/UserFactory.php create mode 100644 database/migrations/2026_08_15_000004_drop_users_and_sessions.php delete mode 100644 routes/auth.php create mode 100644 tests/Feature/DropUsersMigrationTest.php diff --git a/.env.example b/.env.example index d64e7f3..ad12527 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,6 @@ APP_MAINTENANCE_DRIVER=file PHP_CLI_SERVER_WORKERS=4 -BCRYPT_ROUNDS=12 LOG_CHANNEL=stack LOG_STACK=single @@ -27,7 +26,7 @@ DB_DATABASE=incr DB_USERNAME=incr_user DB_PASSWORD=change_me_in_production -SESSION_DRIVER=database +SESSION_DRIVER=cookie SESSION_LIFETIME=120 SESSION_ENCRYPT=true SESSION_PATH=/ diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php deleted file mode 100644 index 5b6256d..0000000 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ /dev/null @@ -1,60 +0,0 @@ -validate([ - 'name' => 'required|string|max:255', - 'email' => 'required|string|lowercase|email|max:255|unique:'.User::class, - 'password' => ['required', 'confirmed', Rules\Password::defaults()], - ]); - - $user = User::forceCreate([ - 'name' => $validated['name'], - 'email' => $validated['email'], - 'password' => Hash::make($validated['password']), - ]); - - event(new Registered($user)); - - Auth::login($user); - - return redirect()->intended(route('dashboard', absolute: false)); - } -} diff --git a/app/Http/Controllers/CounterController.php b/app/Http/Controllers/CounterController.php index 6f1bad9..a2cd706 100644 --- a/app/Http/Controllers/CounterController.php +++ b/app/Http/Controllers/CounterController.php @@ -4,7 +4,7 @@ namespace App\Http\Controllers; -use App\Models\User; +use App\Models\Tracker; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -12,7 +12,7 @@ class CounterController extends Controller { public function increment(): JsonResponse { - $tracker = User::default()->tracker; + $tracker = Tracker::current(); if (! $tracker) { return response()->json(['error' => 'No counter found.'], 404); @@ -30,7 +30,7 @@ public function update(Request $request): JsonResponse 'count' => 'required|integer|min:0|max:4294967295', ]); - $tracker = User::default()->tracker; + $tracker = Tracker::current(); if (! $tracker) { return response()->json(['error' => 'No counter found.'], 404); diff --git a/app/Http/Controllers/TrackerController.php b/app/Http/Controllers/TrackerController.php index 09fa31e..26324db 100644 --- a/app/Http/Controllers/TrackerController.php +++ b/app/Http/Controllers/TrackerController.php @@ -4,7 +4,7 @@ namespace App\Http\Controllers; -use App\Models\User; +use App\Models\Tracker; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -12,7 +12,7 @@ class TrackerController extends Controller { public function show(): JsonResponse { - $tracker = User::default()->tracker; + $tracker = Tracker::current(); if (! $tracker) { return response()->json(['exists' => false]); @@ -28,13 +28,11 @@ public function store(Request $request): JsonResponse 'unit' => 'sometimes|string|max:50', ]); - $user = User::default(); - - if ($user->tracker) { + if (Tracker::current()) { return response()->json(['error' => 'Tracker already exists.'], 409); } - $tracker = $user->tracker()->create([ + $tracker = Tracker::create([ 'label' => $validated['label'] ?? 'Counter', 'unit' => $validated['unit'] ?? 'units', ]); @@ -49,7 +47,7 @@ public function update(Request $request): JsonResponse 'unit' => 'sometimes|string|max:50', ]); - $tracker = User::default()->tracker; + $tracker = Tracker::current(); if (! $tracker) { return response()->json(['error' => 'No counter found.'], 404); diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 784aa7c..bd4569c 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -43,9 +43,6 @@ public function share(Request $request): array ...parent::share($request), 'name' => config('app.name'), 'quote' => ['message' => trim($message), 'author' => trim($author)], - 'auth' => [ - 'user' => $request->user()?->only(['id', 'name', 'email']), - ], 'ziggy' => fn (): array => [ ...(new Ziggy)->toArray(), 'location' => $request->url(), diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php deleted file mode 100644 index 1b02bda..0000000 --- a/app/Http/Requests/Auth/LoginRequest.php +++ /dev/null @@ -1,86 +0,0 @@ -|string> - */ - public function rules(): array - { - return [ - 'email' => ['required', 'string', 'email'], - 'password' => ['required', 'string'], - ]; - } - - /** - * Attempt to authenticate the request's credentials. - * - * @throws ValidationException - */ - public function authenticate(): void - { - $this->ensureIsNotRateLimited(); - - if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { - RateLimiter::hit($this->throttleKey()); - - throw ValidationException::withMessages([ - 'email' => __('auth.failed'), - ]); - } - - RateLimiter::clear($this->throttleKey()); - } - - /** - * Ensure the login request is not rate limited. - * - * @throws ValidationException - */ - public function ensureIsNotRateLimited(): void - { - if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { - return; - } - - event(new Lockout($this)); - - $seconds = RateLimiter::availableIn($this->throttleKey()); - - throw ValidationException::withMessages([ - 'email' => __('auth.throttle', [ - 'seconds' => $seconds, - 'minutes' => ceil($seconds / 60), - ]), - ]); - } - - /** - * Get the rate limiting throttle key for the request. - */ - public function throttleKey(): string - { - return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); - } -} diff --git a/app/Http/Requests/Settings/ProfileUpdateRequest.php b/app/Http/Requests/Settings/ProfileUpdateRequest.php deleted file mode 100644 index 64cf26b..0000000 --- a/app/Http/Requests/Settings/ProfileUpdateRequest.php +++ /dev/null @@ -1,32 +0,0 @@ -|string> - */ - public function rules(): array - { - return [ - 'name' => ['required', 'string', 'max:255'], - - 'email' => [ - 'required', - 'string', - 'lowercase', - 'email', - 'max:255', - Rule::unique(User::class)->ignore($this->user()->id), - ], - ]; - } -} diff --git a/app/Models/Tracker.php b/app/Models/Tracker.php index 8d99c2a..faf7851 100644 --- a/app/Models/Tracker.php +++ b/app/Models/Tracker.php @@ -7,7 +7,6 @@ use Database\Factories\TrackerFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\BelongsTo; class Tracker extends Model { @@ -15,7 +14,6 @@ class Tracker extends Model use HasFactory; protected $fillable = [ - 'user_id', 'label', 'unit', 'count', @@ -28,11 +26,8 @@ protected function casts(): array ]; } - /** - * @return BelongsTo - */ - public function user(): BelongsTo + public static function current(): ?self { - return $this->belongsTo(User::class); + return self::orderBy('id')->first(); } } diff --git a/app/Models/User.php b/app/Models/User.php deleted file mode 100644 index e305b02..0000000 --- a/app/Models/User.php +++ /dev/null @@ -1,52 +0,0 @@ - */ - use HasFactory, Notifiable; - - protected $fillable = [ - 'name', - 'email', - ]; - - protected $hidden = [ - 'password', - 'remember_token', - ]; - - protected function casts(): array - { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; - } - - /** - * @return HasOne - */ - public function tracker(): HasOne - { - return $this->hasOne(Tracker::class); - } - - public static function default(): self - { - return self::firstWhere('email', 'user@incr.local') - ?? self::forceCreate([ - 'email' => 'user@incr.local', - 'name' => 'Default User', - 'password' => bcrypt(Str::random(32)), - ]); - } -} diff --git a/config/auth.php b/config/auth.php deleted file mode 100644 index 9daae00..0000000 --- a/config/auth.php +++ /dev/null @@ -1,117 +0,0 @@ - [ - 'guard' => env('AUTH_GUARD', 'web'), - 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), - ], - - /* - |-------------------------------------------------------------------------- - | Authentication Guards - |-------------------------------------------------------------------------- - | - | Next, you may define every authentication guard for your application. - | Of course, a great default configuration has been defined for you - | which utilizes session storage plus the Eloquent user provider. - | - | All authentication guards have a user provider, which defines how the - | users are actually retrieved out of your database or other storage - | system used by the application. Typically, Eloquent is utilized. - | - | Supported: "session" - | - */ - - 'guards' => [ - 'web' => [ - 'driver' => 'session', - 'provider' => 'users', - ], - ], - - /* - |-------------------------------------------------------------------------- - | User Providers - |-------------------------------------------------------------------------- - | - | All authentication guards have a user provider, which defines how the - | users are actually retrieved out of your database or other storage - | system used by the application. Typically, Eloquent is utilized. - | - | If you have multiple user tables or models you may configure multiple - | providers to represent the model / table. These providers may then - | be assigned to any extra authentication guards you have defined. - | - | Supported: "database", "eloquent" - | - */ - - 'providers' => [ - 'users' => [ - 'driver' => 'eloquent', - 'model' => env('AUTH_MODEL', User::class), - ], - - // 'users' => [ - // 'driver' => 'database', - // 'table' => 'users', - // ], - ], - - /* - |-------------------------------------------------------------------------- - | Resetting Passwords - |-------------------------------------------------------------------------- - | - | These configuration options specify the behavior of Laravel's password - | reset functionality, including the table utilized for token storage - | and the user provider that is invoked to actually retrieve users. - | - | The expiry time is the number of minutes that each reset token will be - | considered valid. This security feature keeps tokens short-lived so - | they have less time to be guessed. You may change this as needed. - | - | The throttle setting is the number of seconds a user must wait before - | generating more password reset tokens. This prevents the user from - | quickly generating a very large amount of password reset tokens. - | - */ - - 'passwords' => [ - 'users' => [ - 'provider' => 'users', - 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), - 'expire' => 60, - 'throttle' => 60, - ], - ], - - /* - |-------------------------------------------------------------------------- - | Password Confirmation Timeout - |-------------------------------------------------------------------------- - | - | Here you may define the amount of seconds before a password confirmation - | window expires and users are asked to re-enter their password via the - | confirmation screen. By default, the timeout lasts for three hours. - | - */ - - 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), - -]; diff --git a/config/session.php b/config/session.php index ba0aa60..2a843ef 100644 --- a/config/session.php +++ b/config/session.php @@ -18,7 +18,7 @@ | */ - 'driver' => env('SESSION_DRIVER', 'database'), + 'driver' => env('SESSION_DRIVER', 'cookie'), /* |-------------------------------------------------------------------------- diff --git a/database/factories/TrackerFactory.php b/database/factories/TrackerFactory.php index 4906e01..d55789d 100644 --- a/database/factories/TrackerFactory.php +++ b/database/factories/TrackerFactory.php @@ -5,7 +5,6 @@ namespace Database\Factories; use App\Models\Tracker; -use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -21,7 +20,6 @@ class TrackerFactory extends Factory public function definition(): array { return [ - 'user_id' => fn () => User::default()->id, 'label' => 'Counter', 'unit' => 'units', 'count' => 0, diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php deleted file mode 100644 index c4ceb07..0000000 --- a/database/factories/UserFactory.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ -class UserFactory extends Factory -{ - /** - * The current password being used by the factory. - */ - protected static ?string $password; - - /** - * Define the model's default state. - * - * @return array - */ - 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, - ]); - } -} diff --git a/database/migrations/2026_08_15_000003_drop_assets_and_pricing.php b/database/migrations/2026_08_15_000003_drop_assets_and_pricing.php index ae3cd5f..00bdd1a 100644 --- a/database/migrations/2026_08_15_000003_drop_assets_and_pricing.php +++ b/database/migrations/2026_08_15_000003_drop_assets_and_pricing.php @@ -44,7 +44,7 @@ public function down(): void }); Schema::table('trackers', function (Blueprint $table): void { - $table->foreignId('asset_id')->nullable()->after('user_id')->constrained()->nullOnDelete(); + $table->foreignId('asset_id')->nullable()->constrained()->nullOnDelete(); $table->boolean('price_tracking_enabled')->default(false); }); } diff --git a/database/migrations/2026_08_15_000004_drop_users_and_sessions.php b/database/migrations/2026_08_15_000004_drop_users_and_sessions.php new file mode 100644 index 0000000..3fca6fa --- /dev/null +++ b/database/migrations/2026_08_15_000004_drop_users_and_sessions.php @@ -0,0 +1,57 @@ +dropForeign(['user_id']); + $table->dropColumn('user_id'); + }); + + Schema::dropIfExists('sessions'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('users'); + } + + // Lossy by design: the app has no users, so nothing is restored into these tables. + public function down(): void + { + Schema::create('users', function (Blueprint $table): void { + $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): void { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table): void { + $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(); + }); + + // Nullable, unlike the original: existing trackers have no user to point at, + // so a NOT NULL foreign key cannot be added back. + Schema::table('trackers', function (Blueprint $table): void { + $table->foreignId('user_id')->nullable()->after('id')->constrained()->cascadeOnDelete(); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index d01a0ef..9c48834 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,8 +2,7 @@ namespace Database\Seeders; -use App\Models\User; -// use Illuminate\Database\Console\Seeds\WithoutModelEvents; +use App\Models\Tracker; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder @@ -13,11 +12,10 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // User::factory(10)->create(); - - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + Tracker::firstOrCreate([], [ + 'label' => 'Counter', + 'unit' => 'units', + 'count' => 0, ]); } } diff --git a/routes/auth.php b/routes/auth.php deleted file mode 100644 index 7a13564..0000000 --- a/routes/auth.php +++ /dev/null @@ -1,8 +0,0 @@ -name('register'); -Route::post('register', [RegisteredUserController::class, 'store']); diff --git a/routes/web.php b/routes/web.php index 5a282c1..11f3f02 100644 --- a/routes/web.php +++ b/routes/web.php @@ -21,5 +21,3 @@ // Counter routes Route::post('/increment', [CounterController::class, 'increment'])->name('counter.increment'); Route::patch('/count', [CounterController::class, 'update'])->name('counter.update'); - -require __DIR__.'/auth.php'; diff --git a/tests/Feature/CountBackfillMigrationTest.php b/tests/Feature/CountBackfillMigrationTest.php index cb758e6..8b826d4 100644 --- a/tests/Feature/CountBackfillMigrationTest.php +++ b/tests/Feature/CountBackfillMigrationTest.php @@ -32,16 +32,7 @@ private function revertToLedger(): void private function makeTracker(): int { - $userId = DB::table('users')->insertGetId([ - 'name' => 'Test', - 'email' => 'ledger@example.test', - 'password' => 'x', - 'created_at' => now(), - 'updated_at' => now(), - ]); - return DB::table('trackers')->insertGetId([ - 'user_id' => $userId, 'label' => 'Counter', 'unit' => 'units', 'created_at' => now(), diff --git a/tests/Feature/DropAssetsMigrationTest.php b/tests/Feature/DropAssetsMigrationTest.php index 2dda24c..da17be1 100644 --- a/tests/Feature/DropAssetsMigrationTest.php +++ b/tests/Feature/DropAssetsMigrationTest.php @@ -53,16 +53,7 @@ public function test_dropping_assets_does_not_disturb_the_counter(): void { $this->migration()->down(); - $userId = DB::table('users')->insertGetId([ - 'name' => 'Test', - 'email' => 'assets@example.test', - 'password' => 'x', - 'created_at' => now(), - 'updated_at' => now(), - ]); - $trackerId = DB::table('trackers')->insertGetId([ - 'user_id' => $userId, 'label' => 'Counter', 'unit' => 'units', 'count' => 31, diff --git a/tests/Feature/DropUsersMigrationTest.php b/tests/Feature/DropUsersMigrationTest.php new file mode 100644 index 0000000..f642a1e --- /dev/null +++ b/tests/Feature/DropUsersMigrationTest.php @@ -0,0 +1,86 @@ +assertFalse(Schema::hasTable('users')); + $this->assertFalse(Schema::hasTable('sessions')); + $this->assertFalse(Schema::hasTable('password_reset_tokens')); + $this->assertFalse(Schema::hasColumn('trackers', 'user_id')); + } + + public function test_down_restores_the_tables_and_column(): void + { + $this->migration()->down(); + + $this->assertTrue(Schema::hasTable('users')); + $this->assertTrue(Schema::hasTable('sessions')); + $this->assertTrue(Schema::hasTable('password_reset_tokens')); + $this->assertTrue(Schema::hasColumn('trackers', 'user_id')); + } + + public function test_restored_user_id_is_nullable_so_existing_trackers_survive(): void + { + DB::table('trackers')->insert([ + 'label' => 'Counter', + 'unit' => 'units', + 'count' => 3, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->migration()->down(); + + $nullable = DB::selectOne( + 'select is_nullable from information_schema.columns + where table_schema = database() and table_name = ? and column_name = ?', + ['trackers', 'user_id'] + ); + + $this->assertSame('YES', $nullable->is_nullable ?? $nullable->IS_NULLABLE); + } + + public function test_up_drops_them_again_respecting_foreign_keys(): void + { + $this->migration()->down(); + $this->migration()->up(); + + $this->assertFalse(Schema::hasTable('users')); + $this->assertFalse(Schema::hasColumn('trackers', 'user_id')); + } + + public function test_the_counter_survives_the_round_trip(): void + { + $trackerId = DB::table('trackers')->insertGetId([ + 'label' => 'Counter', + 'unit' => 'units', + 'count' => 77, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->migration()->down(); + $this->migration()->up(); + + $this->assertSame(77, (int) DB::table('trackers')->where('id', $trackerId)->value('count')); + } +}