Removing 4 word bollocks and fixing rootless supervisor for podman

This commit is contained in:
Dan Baker 2026-02-22 19:22:00 +00:00
parent a22db4ee0f
commit 82ed2e3ce2
8 changed files with 26 additions and 160 deletions

View file

@ -51,7 +51,9 @@ class MagicLinkController extends Controller
// Queue the magic link email // Queue the magic link email
Mail::to($email)->queue(new MagicLoginLink($loginUrl, $token->plain_code, 15)); Mail::to($email)->queue(new MagicLoginLink($loginUrl, $token->plain_code, 15));
return back()->with('status', 'Check your email for a login link and code!'); return redirect()->route('verify-code')
->with('status', 'Check your email for your login code!')
->with('email', $email);
} catch (ValidationException $e) { } catch (ValidationException $e) {
throw $e; throw $e;
} }

View file

@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str;
class MagicLoginToken extends Model class MagicLoginToken extends Model
{ {
@ -48,13 +48,13 @@ class MagicLoginToken extends Model
public static function generate(string $email, ?string $ip = null, ?string $ua = null): self public static function generate(string $email, ?string $ip = null, ?string $ua = null): self
{ {
$emailHash = User::hashEmail($email); $emailHash = User::hashEmail($email);
$wordToken = self::generateWordToken(); $token = Str::random(64);
$code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
return self::create([ return self::create([
'email_hash' => $emailHash, 'email_hash' => $emailHash,
'token_hash' => Hash::make($wordToken), 'token_hash' => Hash::make($token),
'plain_token' => $wordToken, 'plain_token' => $token,
'code_hash' => Hash::make($code), 'code_hash' => Hash::make($code),
'plain_code' => $code, 'plain_code' => $code,
'expires_at' => now()->addMinutes(15), 'expires_at' => now()->addMinutes(15),
@ -63,27 +63,6 @@ class MagicLoginToken extends Model
]); ]);
} }
/**
* Generate a unique 4-word token from the word list.
*/
public static function generateWordToken(): string
{
$words = explode("\n", trim(Storage::get('words.txt')));
do {
$selectedWords = [];
for ($i = 0; $i < 4; $i++) {
$selectedWords[] = $words[array_rand($words)];
}
$token = implode('-', $selectedWords);
// Check for uniqueness in database
$exists = self::where('plain_token', $token)->exists();
} while ($exists);
return $token;
}
/** /**
* Check if the token is valid (not expired and not used). * Check if the token is valid (not expired and not used).
*/ */

View file

@ -1,9 +1,19 @@
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0770
[supervisord] [supervisord]
nodaemon=true nodaemon=true
logfile=/dev/stdout logfile=/dev/stdout
logfile_maxbytes=0 logfile_maxbytes=0
pidfile=/tmp/supervisord.pid pidfile=/tmp/supervisord.pid
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock
[program:php-fpm] [program:php-fpm]
command=/usr/local/sbin/php-fpm -F command=/usr/local/sbin/php-fpm -F
autostart=true autostart=true
@ -26,6 +36,8 @@ stderr_logfile_maxbytes=0
command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600 command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true autostart=true
autorestart=true autorestart=true
startsecs=0
startretries=10
stdout_logfile=/dev/stdout stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0 stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr stderr_logfile=/dev/stderr

View file

@ -38,7 +38,7 @@
type="email" type="email"
id="email" id="email"
name="email" name="email"
value="{{ old('email') }}" value="{{ old('email', session('email')) }}"
required required
autofocus autofocus
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors @error('email') border-red-500 @enderror" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors @error('email') border-red-500 @enderror"

View file

@ -1,56 +0,0 @@
#!/bin/bash
# Helper script to start Laravel containers with Podman
# Use flatpak-spawn to run podman commands on the host
PODMAN="flatpak-spawn --host podman"
echo "Creating network if it doesn't exist..."
$PODMAN network exists laravel || $PODMAN network create laravel
echo "Building application image..."
$PODMAN build -t laravel-app .
echo "Starting MySQL database..."
$PODMAN run -d \
--name laravel-db \
--network laravel \
--replace \
-e MYSQL_DATABASE=laravel \
-e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_USER=laravel \
-e MYSQL_PASSWORD=secret \
-p 3306:3306 \
-v laravel-dbdata:/var/lib/mysql \
docker.io/library/mysql:8.0
echo "Starting Redis..."
$PODMAN run -d \
--name laravel-redis \
--network laravel \
--replace \
-p 6379:6379 \
docker.io/library/redis:alpine
echo "Waiting for database to be ready..."
sleep 10
echo "Starting Laravel application..."
$PODMAN run -d \
--name laravel-app \
--network laravel \
--replace \
-v "$(pwd)":/var/www/html:z \
-p 8080:80 \
laravel-app
echo ""
echo "✓ All containers started successfully!"
echo ""
echo "Access your Laravel application at: http://localhost:8080"
echo ""
echo "Useful commands:"
echo " View logs: flatpak-spawn --host podman logs -f laravel-app"
echo " Run artisan: flatpak-spawn --host podman exec laravel-app php artisan [command]"
echo " Run migrations: flatpak-spawn --host podman exec laravel-app php artisan migrate"
echo " Stop containers: ./stop.sh"
echo ""

13
stop.sh
View file

@ -1,13 +0,0 @@
#!/bin/bash
# Helper script to stop Laravel containers
PODMAN="flatpak-spawn --host podman"
echo "Stopping containers..."
$PODMAN stop laravel-app laravel-db laravel-redis 2>/dev/null
echo "Removing containers..."
$PODMAN rm laravel-app laravel-db laravel-redis 2>/dev/null
echo "✓ Containers stopped and removed"
echo ""
echo "Note: Database data is preserved in the 'laravel-dbdata' volume"
echo "To remove the volume and delete all data: flatpak-spawn --host podman volume rm laravel-dbdata"

View file

@ -40,8 +40,8 @@ class MagicLinkAuthTest extends TestCase
'email' => 'test@example.com', 'email' => 'test@example.com',
]); ]);
$response->assertRedirect(); $response->assertRedirect(route('verify-code'));
$response->assertSessionHas('status', 'Check your email for a login link and code!'); $response->assertSessionHas('status', 'Check your email for your login code!');
Mail::assertQueued(MagicLoginLink::class, function ($mail) { Mail::assertQueued(MagicLoginLink::class, function ($mail) {
return $mail->hasTo('test@example.com'); return $mail->hasTo('test@example.com');
@ -71,7 +71,7 @@ class MagicLinkAuthTest extends TestCase
]); ]);
} }
public function test_valid_4_word_token_logs_user_in(): void public function test_valid_token_logs_user_in(): void
{ {
$user = User::create([ $user = User::create([
'email_hash' => User::hashEmail('test@example.com'), 'email_hash' => User::hashEmail('test@example.com'),

View file

@ -6,51 +6,13 @@ use App\Models\MagicLoginToken;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str;
use Tests\TestCase; use Tests\TestCase;
class MagicLoginTokenTest extends TestCase class MagicLoginTokenTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
public function test_word_token_generation_creates_exactly_4_words(): void
{
$token = MagicLoginToken::generateWordToken();
$words = explode('-', $token);
$this->assertCount(4, $words, 'Token should contain exactly 4 words');
}
public function test_word_token_is_hyphen_separated(): void
{
$token = MagicLoginToken::generateWordToken();
$this->assertMatchesRegularExpression(
'/^[a-z]+-[a-z]+-[a-z]+-[a-z]+$/',
$token,
'Token should match pattern: word-word-word-word'
);
}
public function test_words_are_from_word_list_file(): void
{
$wordList = explode("\n", trim(Storage::get('words.txt')));
$wordList = array_map('trim', $wordList);
$wordList = array_filter($wordList);
$token = MagicLoginToken::generateWordToken();
$words = explode('-', $token);
foreach ($words as $word) {
$this->assertContains(
$word,
$wordList,
"Word '{$word}' should be from the word list file"
);
}
}
public function test_token_expiry_validation_works(): void public function test_token_expiry_validation_works(): void
{ {
$token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent'); $token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent');
@ -125,7 +87,7 @@ class MagicLoginTokenTest extends TestCase
); );
$this->assertFalse( $this->assertFalse(
$token->verifyToken('wrong-token-here-bad'), $token->verifyToken(Str::random(64)),
'Incorrect token should not verify' 'Incorrect token should not verify'
); );
} }
@ -270,7 +232,7 @@ class MagicLoginTokenTest extends TestCase
$this->assertEquals($token1->email_hash, $token2->email_hash); $this->assertEquals($token1->email_hash, $token2->email_hash);
} }
public function test_word_tokens_are_unique(): void public function test_tokens_are_unique(): void
{ {
$generatedTokens = []; $generatedTokens = [];
@ -301,24 +263,4 @@ class MagicLoginTokenTest extends TestCase
$this->assertInstanceOf(\Illuminate\Support\Carbon::class, $token->used_at); $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');
}
}
} }