fix(bio): prevent duplicate slugs within same workspace

- Migration: add UNIQUE(workspace_id, slug) to bio_pages
  Old (subdomain_host, slug) unique allowed MySQL NULL bypass —
  two free-plan bios with NULL subdomain_host + same slug were not caught
- Edit::save(): validate slug uniqueness before update, dispatch toast on conflict
- Edit::save(): validate slug format (alphanumeric + dash/underscore, max 64)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-17 23:09:17 +02:00
parent deb5f310a1
commit 308778b99d
2 changed files with 46 additions and 1 deletions

View File

@ -43,10 +43,30 @@ class Edit extends Component
{
$page = BioPage::findOrFail($this->pageId);
$newSlug = $state['slug'] ?? $page->slug;
$slugTaken = BioPage::where('workspace_id', $page->workspace_id)
->where('slug', $newSlug)
->where('id', '!=', $page->id)
->whereNull('deleted_at')
->exists();
if ($slugTaken) {
$this->dispatch('toast', message: 'Slug "' . $newSlug . '" ist bereits vergeben.', type: 'error');
return;
}
if (! preg_match('/^[a-zA-Z0-9_-]{1,64}$/', $newSlug)) {
$this->dispatch('toast', message: 'Slug: nur Buchstaben, Zahlen, - und _. Max 64 Zeichen.', type: 'error');
return;
}
$displayName = $state['profile']['displayName'] ?? '';
$page->update([
'slug' => $state['slug'] ?? $page->slug,
'slug' => $newSlug,
'title' => ['en' => $displayName, 'de' => $displayName],
'is_published' => false,
'theme' => [

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('bio_pages', function (Blueprint $table) {
// Slug must be unique within a workspace regardless of subdomain_host.
// The old (subdomain_host, slug) unique allowed MySQL NULL bypass —
// two rows with NULL subdomain_host + same slug were not caught.
$table->unique(['workspace_id', 'slug'], 'bio_pages_workspace_slug_unique');
});
}
public function down(): void
{
Schema::table('bio_pages', function (Blueprint $table) {
$table->dropUnique('bio_pages_workspace_slug_unique');
});
}
};