diff --git a/.gitignore b/.gitignore index b71b1ea..37ac6c0 100755 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .env.production .phpactor.json .phpunit.result.cache +/.composer /.fleet /.idea /.nova diff --git a/Dockerfile b/Dockerfile index 2a9751c..f5c849b 100755 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,10 @@ RUN apt-get update && apt-get install -y \ # Clear cache RUN apt-get clean && rm -rf /var/lib/apt/lists/* +# Fix nginx permissions for rootless +RUN mkdir -p /var/lib/nginx/body /var/lib/nginx/proxy /var/lib/nginx/fastcgi /var/log/nginx /var/run \ + && chmod -R 777 /var/lib/nginx /var/log/nginx /var/run + # Install PHP extensions RUN docker-php-ext-install pdo_mysql pdo_pgsql mbstring exif pcntl bcmath gd zip @@ -42,7 +46,7 @@ RUN chown -R www-data:www-data /var/www/html \ && chmod -R 755 /var/www/html/bootstrap/cache # Expose port 80 -EXPOSE 80 +EXPOSE 8080 # Start supervisor CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/compose.yml b/compose.yml index 41ed6b5..d44a9a5 100755 --- a/compose.yml +++ b/compose.yml @@ -5,11 +5,12 @@ services: dockerfile: Dockerfile container_name: laravel-app restart: unless-stopped + userns_mode: "keep-id" working_dir: /var/www/html volumes: - ./:/var/www/html:Z ports: - - "8080:80" + - "8080:8080" networks: - laravel depends_on: @@ -48,11 +49,26 @@ services: container_name: laravel-mailpit restart: unless-stopped ports: - - "8025:8025" # Web UI - - "1025:1025" # SMTP server + - "8025:8025" # Web UI + - "1025:1025" # SMTP server networks: - laravel + phpmyadmin: + image: docker.io/phpmyadmin/phpmyadmin:latest + container_name: laravel-phpmyadmin + restart: unless-stopped + environment: + PMA_HOST: db + PMA_USER: root + PMA_PASSWORD: root + ports: + - "8081:80" + networks: + - laravel + depends_on: + - db + networks: laravel: driver: bridge diff --git a/composer.json b/composer.json index 52a3a8a..b85984c 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ "require": { "php": "^8.2", "laravel/framework": "^12.0", - "laravel/tinker": "^2.10.1" + "laravel/tinker": "^2.10.1", + "predis/predis": "^3.4" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index 776a825..35454cf 100755 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c514d8f7b9fc5970bdd94287905ef584", + "content-hash": "d6bd2a26512dd52b870be9ab0b7d9c72", "packages": [ { "name": "brick/math", @@ -2603,6 +2603,69 @@ ], "time": "2025-12-27T19:41:33+00:00" }, + { + "name": "predis/predis", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/predis/predis.git", + "reference": "1183f5732e6b10efd33f64984a96726eaecb59aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/predis/predis/zipball/1183f5732e6b10efd33f64984a96726eaecb59aa", + "reference": "1183f5732e6b10efd33f64984a96726eaecb59aa", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.0|^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.3", + "phpstan/phpstan": "^1.9", + "phpunit/phpcov": "^6.0 || ^8.0", + "phpunit/phpunit": "^8.0 || ~9.4.4" + }, + "suggest": { + "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Till Krüss", + "homepage": "https://till.im", + "role": "Maintainer" + } + ], + "description": "A flexible and feature-complete Redis/Valkey client for PHP.", + "homepage": "http://github.com/predis/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "support": { + "issues": "https://github.com/predis/predis/issues", + "source": "https://github.com/predis/predis/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/tillkruss", + "type": "github" + } + ], + "time": "2026-02-11T17:30:28+00:00" + }, { "name": "psr/clock", "version": "1.0.0", diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index 76454e6..8fb0af6 100755 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -1,6 +1,6 @@ server { - listen 80; - listen [::]:80; + listen 8080; + listen [::]:8080; server_name localhost; root /var/www/html/public; diff --git a/docker/supervisor/supervisord.conf b/docker/supervisor/supervisord.conf index 80f9c3f..9e0a822 100755 --- a/docker/supervisor/supervisord.conf +++ b/docker/supervisor/supervisord.conf @@ -1,8 +1,8 @@ [supervisord] nodaemon=true -user=root -logfile=/var/log/supervisor/supervisord.log -pidfile=/var/run/supervisord.pid +logfile=/dev/stdout +logfile_maxbytes=0 +pidfile=/tmp/supervisord.pid [program:php-fpm] command=/usr/local/sbin/php-fpm -F @@ -21,3 +21,12 @@ stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 + +[program:queue-worker] +command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600 +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 04fd4c8..d703241 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -23,6 +23,8 @@ + + diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index de23350..b6cc4b0 100755 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -27,7 +27,7 @@ @endif -
+ @csrf
diff --git a/routes/web.php b/routes/web.php index b33be3e..2e741f4 100755 --- a/routes/web.php +++ b/routes/web.php @@ -10,7 +10,7 @@ Route::get('/', function () { // Guest routes (unauthenticated users) Route::middleware('guest')->group(function () { Route::get('/login', [MagicLinkController::class, 'showLoginForm'])->name('login'); - Route::post('/login', [MagicLinkController::class, 'sendLink'])->name('magic-link.send'); + Route::post('/magic-link', [MagicLinkController::class, 'sendLink'])->name('magic-link.send'); Route::get('/verify-code', [MagicLinkController::class, 'showCodeForm'])->name('verify-code'); Route::post('/verify-code', [MagicLinkController::class, 'verifyCode'])->name('magic-link.verify-code'); Route::get('/auth/magic-link', [MagicLinkController::class, 'verifyLink'])->name('magic-link.verify'); diff --git a/tests/Feature/Auth/MagicLinkAuthTest.php b/tests/Feature/Auth/MagicLinkAuthTest.php index 2900f9c..b53803d 100755 --- a/tests/Feature/Auth/MagicLinkAuthTest.php +++ b/tests/Feature/Auth/MagicLinkAuthTest.php @@ -7,7 +7,6 @@ use App\Models\MagicLoginToken; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\URL; @@ -165,27 +164,6 @@ class MagicLinkAuthTest extends TestCase $this->assertGuest(); } - public function test_4_word_token_format_validation(): void - { - $token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent'); - - $plainToken = $token->plain_token; - - $this->assertMatchesRegularExpression( - '/^[a-z]+-[a-z]+-[a-z]+-[a-z]+$/', - $plainToken, - 'Token should be 4 words separated by hyphens' - ); - - $words = explode('-', $plainToken); - $this->assertCount(4, $words, 'Token should contain exactly 4 words'); - - foreach ($words as $word) { - $this->assertNotEmpty($word, 'Each word should not be empty'); - $this->assertMatchesRegularExpression('/^[a-z]+$/', $word, 'Each word should contain only lowercase letters'); - } - } - public function test_remember_token_always_set(): void { $user = User::create([ diff --git a/tests/TestCase.php b/tests/TestCase.php index c58ad54..2932d4a 100755 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,11 +2,9 @@ namespace Tests; -use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { use CreatesApplication; - use DatabaseTransactions; } diff --git a/tests/Unit/MagicLoginTokenTest.php b/tests/Unit/MagicLoginTokenTest.php index 6c8372e..794b9c8 100755 --- a/tests/Unit/MagicLoginTokenTest.php +++ b/tests/Unit/MagicLoginTokenTest.php @@ -219,7 +219,7 @@ class MagicLoginTokenTest extends TestCase public function test_token_expiry_is_15_minutes(): void { - $beforeCreation = now()->addMinutes(15); + $beforeCreation = now()->startOfSecond()->addMinutes(15); $token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent'); @@ -300,4 +300,25 @@ class MagicLoginTokenTest extends TestCase $this->assertInstanceOf(\Illuminate\Support\Carbon::class, $token->used_at); } + + public function test_4_word_token_format_validation(): void + { + $token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent'); + + $plainToken = $token->plain_token; + + $this->assertMatchesRegularExpression( + '/^[a-z]+-[a-z]+-[a-z]+-[a-z]+$/', + $plainToken, + 'Token should be 4 words separated by hyphens' + ); + + $words = explode('-', $plainToken); + $this->assertCount(4, $words, 'Token should contain exactly 4 words'); + + foreach ($words as $word) { + $this->assertNotEmpty($word, 'Each word should not be empty'); + $this->assertMatchesRegularExpression('/^[a-z]+$/', $word, 'Each word should contain only lowercase letters'); + } + } }