450 lines
15 KiB
Markdown
450 lines
15 KiB
Markdown
# Story 6.9: Legal Pages Editor
|
|
|
|
## Epic Reference
|
|
**Epic 6:** Admin Dashboard
|
|
|
|
## User Story
|
|
As an **admin**,
|
|
I want **to edit Terms of Service and Privacy Policy pages**,
|
|
So that **I can maintain legal compliance and update policies**.
|
|
|
|
## Dependencies
|
|
- **Epic 1:** Base authentication and admin middleware
|
|
- **Story 6.8:** System Settings (admin settings UI patterns and navigation)
|
|
|
|
## References
|
|
- **PRD Section 10.1:** Legal & Compliance - Required pages specification
|
|
- **PRD Section 5.7H:** Settings - Terms of Service and Privacy Policy editor requirements
|
|
- **PRD Section 9.3:** User Privacy - Terms and Privacy page requirements
|
|
- **PRD Section 16.3:** Third-party dependencies - Rich text editor options (TinyMCE or Quill)
|
|
|
|
## Acceptance Criteria
|
|
|
|
### Pages to Edit
|
|
- [x] Terms of Service (`/page/terms`)
|
|
- [x] Privacy Policy (`/page/privacy`)
|
|
|
|
### Editor Features
|
|
- [x] Rich text editor using Quill.js (lightweight, RTL-friendly)
|
|
- [x] Bilingual content with Arabic/English tabs in editor UI
|
|
- [x] Save and publish button updates database immediately
|
|
- [x] Preview opens modal showing rendered content in selected language
|
|
- [x] HTML content sanitized before save (prevent XSS)
|
|
|
|
### Admin UI Location
|
|
- [x] Accessible under Admin Dashboard > Settings section
|
|
- [x] Sidebar item: "Legal Pages" or integrate into Settings page
|
|
- [x] List view showing both pages with "Edit" action
|
|
- [x] Edit page shows language tabs (Arabic | English) above editor
|
|
|
|
### Public Display
|
|
- [x] Pages accessible from footer links (no auth required)
|
|
- [x] Route: `/page/{slug}` where slug is `terms` or `privacy`
|
|
- [x] Content displayed in user's current language preference
|
|
- [x] Last updated timestamp displayed at bottom of page
|
|
- [x] Professional layout consistent with site design (navy/gold)
|
|
|
|
## Technical Notes
|
|
|
|
### Architecture
|
|
This project uses **class-based Volt components** for interactivity. Follow existing patterns in `resources/views/livewire/`.
|
|
|
|
### Rich Text Editor
|
|
Use **Quill.js** for the rich text editor:
|
|
- Lightweight and RTL-compatible
|
|
- Include via CDN or npm
|
|
- Configure toolbar: bold, italic, underline, lists, links, headings
|
|
- Bind to Livewire with `wire:model` on hidden textarea
|
|
|
|
### Model & Migration
|
|
|
|
```php
|
|
// Migration: create_pages_table.php
|
|
Schema::create('pages', function (Blueprint $table) {
|
|
$table->id();
|
|
$table->string('slug')->unique(); // 'terms', 'privacy'
|
|
$table->string('title_ar');
|
|
$table->string('title_en');
|
|
$table->longText('content_ar')->nullable();
|
|
$table->longText('content_en')->nullable();
|
|
$table->timestamps(); // updated_at used for "Last updated"
|
|
});
|
|
```
|
|
|
|
### Seeder
|
|
|
|
```php
|
|
// PageSeeder.php
|
|
Page::create([
|
|
'slug' => 'terms',
|
|
'title_ar' => 'شروط الخدمة',
|
|
'title_en' => 'Terms of Service',
|
|
'content_ar' => '',
|
|
'content_en' => '',
|
|
]);
|
|
|
|
Page::create([
|
|
'slug' => 'privacy',
|
|
'title_ar' => 'سياسة الخصوصية',
|
|
'title_en' => 'Privacy Policy',
|
|
'content_ar' => '',
|
|
'content_en' => '',
|
|
]);
|
|
```
|
|
|
|
### Routes
|
|
|
|
```php
|
|
// Public route (web.php)
|
|
Route::get('/page/{slug}', [PageController::class, 'show'])
|
|
->name('page.show')
|
|
->where('slug', 'terms|privacy');
|
|
|
|
// Admin route (protected by admin middleware)
|
|
Route::middleware(['auth', 'admin'])->prefix('admin')->group(function () {
|
|
Route::get('/pages', PagesIndex::class)->name('admin.pages.index');
|
|
Route::get('/pages/{slug}/edit', PagesEdit::class)->name('admin.pages.edit');
|
|
});
|
|
```
|
|
|
|
### Volt Component Structure
|
|
|
|
```php
|
|
// resources/views/livewire/admin/pages/edit.blade.php
|
|
<?php
|
|
use Livewire\Volt\Component;
|
|
use App\Models\Page;
|
|
|
|
new class extends Component {
|
|
public Page $page;
|
|
public string $activeTab = 'ar';
|
|
public string $content_ar = '';
|
|
public string $content_en = '';
|
|
public bool $showPreview = false;
|
|
|
|
public function mount(string $slug): void
|
|
{
|
|
$this->page = Page::where('slug', $slug)->firstOrFail();
|
|
$this->content_ar = $this->page->content_ar ?? '';
|
|
$this->content_en = $this->page->content_en ?? '';
|
|
}
|
|
|
|
public function save(): void
|
|
{
|
|
$this->page->update([
|
|
'content_ar' => clean($this->content_ar), // Sanitize HTML
|
|
'content_en' => clean($this->content_en),
|
|
]);
|
|
|
|
$this->dispatch('notify', message: __('Page saved successfully'));
|
|
}
|
|
|
|
public function togglePreview(): void
|
|
{
|
|
$this->showPreview = !$this->showPreview;
|
|
}
|
|
}; ?>
|
|
```
|
|
|
|
### HTML Sanitization
|
|
Use `mews/purifier` package or similar to sanitize rich text HTML before saving:
|
|
```bash
|
|
composer require mews/purifier
|
|
```
|
|
|
|
### Edge Cases
|
|
- **Empty content:** Allow saving empty content (legal pages may be drafted later)
|
|
- **Large content:** Use `longText` column type, consider lazy loading for edit
|
|
- **Concurrent edits:** Single admin system - not a concern
|
|
- **RTL in editor:** Quill supports RTL via `direction: rtl` CSS on editor container
|
|
|
|
## Test Scenarios
|
|
|
|
### Feature Tests
|
|
|
|
```php
|
|
// tests/Feature/Admin/LegalPagesTest.php
|
|
|
|
test('admin can view legal pages list', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
|
|
$this->actingAs($admin)
|
|
->get(route('admin.pages.index'))
|
|
->assertOk()
|
|
->assertSee('Terms of Service')
|
|
->assertSee('Privacy Policy');
|
|
});
|
|
|
|
test('admin can edit terms of service in Arabic', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$page = Page::where('slug', 'terms')->first();
|
|
|
|
Volt::test('admin.pages.edit', ['slug' => 'terms'])
|
|
->actingAs($admin)
|
|
->set('content_ar', '<p>شروط الخدمة الجديدة</p>')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
expect($page->fresh()->content_ar)->toContain('شروط الخدمة الجديدة');
|
|
});
|
|
|
|
test('admin can edit terms of service in English', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$page = Page::where('slug', 'terms')->first();
|
|
|
|
Volt::test('admin.pages.edit', ['slug' => 'terms'])
|
|
->actingAs($admin)
|
|
->set('content_en', '<p>New terms content</p>')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
expect($page->fresh()->content_en)->toContain('New terms content');
|
|
});
|
|
|
|
test('admin can preview page content', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
|
|
Volt::test('admin.pages.edit', ['slug' => 'terms'])
|
|
->actingAs($admin)
|
|
->call('togglePreview')
|
|
->assertSet('showPreview', true);
|
|
});
|
|
|
|
test('updated_at timestamp changes on save', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$page = Page::where('slug', 'terms')->first();
|
|
$originalTimestamp = $page->updated_at;
|
|
|
|
$this->travel(1)->minute();
|
|
|
|
Volt::test('admin.pages.edit', ['slug' => 'terms'])
|
|
->actingAs($admin)
|
|
->set('content_en', '<p>Updated content</p>')
|
|
->call('save');
|
|
|
|
expect($page->fresh()->updated_at)->toBeGreaterThan($originalTimestamp);
|
|
});
|
|
|
|
test('public can view terms page', function () {
|
|
Page::where('slug', 'terms')->update(['content_en' => '<p>Our terms</p>']);
|
|
|
|
$this->get('/page/terms')
|
|
->assertOk()
|
|
->assertSee('Our terms');
|
|
});
|
|
|
|
test('public can view privacy page', function () {
|
|
Page::where('slug', 'privacy')->update(['content_en' => '<p>Our privacy policy</p>']);
|
|
|
|
$this->get('/page/privacy')
|
|
->assertOk()
|
|
->assertSee('Our privacy policy');
|
|
});
|
|
|
|
test('public page shows last updated timestamp', function () {
|
|
$page = Page::where('slug', 'terms')->first();
|
|
|
|
$this->get('/page/terms')
|
|
->assertOk()
|
|
->assertSee($page->updated_at->format('M d, Y'));
|
|
});
|
|
|
|
test('invalid page slug returns 404', function () {
|
|
$this->get('/page/invalid-slug')
|
|
->assertNotFound();
|
|
});
|
|
|
|
test('html content is sanitized on save', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
|
|
Volt::test('admin.pages.edit', ['slug' => 'terms'])
|
|
->actingAs($admin)
|
|
->set('content_en', '<script>alert("xss")</script><p>Safe content</p>')
|
|
->call('save');
|
|
|
|
$page = Page::where('slug', 'terms')->first();
|
|
expect($page->content_en)->not->toContain('<script>');
|
|
expect($page->content_en)->toContain('Safe content');
|
|
});
|
|
|
|
test('guest cannot access admin pages editor', function () {
|
|
$this->get(route('admin.pages.index'))
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('client cannot access admin pages editor', function () {
|
|
$client = User::factory()->create();
|
|
|
|
$this->actingAs($client)
|
|
->get(route('admin.pages.index'))
|
|
->assertForbidden();
|
|
});
|
|
```
|
|
|
|
## Definition of Done
|
|
- [x] Page model and migration created
|
|
- [x] Seeder creates terms and privacy pages
|
|
- [x] Admin can access legal pages list from settings/sidebar
|
|
- [x] Admin can edit Terms of Service content (Arabic)
|
|
- [x] Admin can edit Terms of Service content (English)
|
|
- [x] Admin can edit Privacy Policy content (Arabic)
|
|
- [x] Admin can edit Privacy Policy content (English)
|
|
- [x] Language tabs switch between Arabic/English editor
|
|
- [x] Rich text editor (Quill) works with RTL Arabic text
|
|
- [x] Preview modal displays rendered content
|
|
- [x] Save button updates database and shows success notification
|
|
- [x] HTML content sanitized before save
|
|
- [x] Public `/page/terms` route displays Terms of Service
|
|
- [x] Public `/page/privacy` route displays Privacy Policy
|
|
- [x] Public pages show content in user's language preference
|
|
- [x] Last updated timestamp displayed on public pages
|
|
- [x] Footer links to legal pages work
|
|
- [x] All feature tests pass
|
|
- [x] Code formatted with Pint
|
|
|
|
## Estimation
|
|
**Complexity:** Medium | **Effort:** 4-5 hours
|
|
|
|
## Out of Scope
|
|
- Version history for legal pages
|
|
- Scheduled publishing
|
|
- Multiple admin approval workflow
|
|
- PDF export of legal pages
|
|
|
|
---
|
|
|
|
## Dev Agent Record
|
|
|
|
### Status
|
|
Ready for Review
|
|
|
|
### Agent Model Used
|
|
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
|
|
|
### Completion Notes
|
|
- Page model with localized title/content accessors created
|
|
- PageSeeder creates terms and privacy pages with Arabic/English titles
|
|
- Installed mews/purifier for HTML sanitization via clean() helper
|
|
- Admin pages index shows both legal pages with edit actions
|
|
- Admin pages edit uses Quill.js rich text editor with RTL support for Arabic
|
|
- Language tabs (Arabic/English) switch editor content
|
|
- Preview modal displays sanitized content in both languages
|
|
- Public route `/page/{slug}` displays content based on user's locale
|
|
- Legacy `/terms` and `/privacy` routes redirect to new dynamic routes
|
|
- Footer links already pointed to correct routes (via redirect)
|
|
- 29 feature tests covering all acceptance criteria
|
|
- All tests pass, code formatted with Pint
|
|
|
|
### File List
|
|
**New Files:**
|
|
- `app/Models/Page.php` - Page model with localized accessors
|
|
- `database/factories/PageFactory.php` - Factory with terms/privacy states
|
|
- `database/migrations/2025_12_28_203026_create_pages_table.php` - Pages table migration
|
|
- `database/seeders/PageSeeder.php` - Seeds terms and privacy pages
|
|
- `resources/views/livewire/admin/pages/index.blade.php` - Admin pages list
|
|
- `resources/views/livewire/admin/pages/edit.blade.php` - Admin page editor with Quill
|
|
- `resources/views/pages/legal.blade.php` - Public legal page view
|
|
- `lang/en/pages.php` - English translations for pages feature
|
|
- `lang/ar/pages.php` - Arabic translations for pages feature
|
|
- `tests/Feature/Admin/LegalPagesTest.php` - Feature tests (29 tests)
|
|
|
|
**Modified Files:**
|
|
- `routes/web.php` - Added admin pages routes and public `/page/{slug}` route
|
|
- `resources/views/components/layouts/app/sidebar.blade.php` - Added Legal Pages nav item
|
|
- `database/seeders/DatabaseSeeder.php` - Added PageSeeder call
|
|
- `lang/en/navigation.php` - Added legal_pages key
|
|
- `lang/ar/navigation.php` - Added legal_pages key
|
|
|
|
### Change Log
|
|
- 2025-12-28: Initial implementation of Story 6.9 Legal Pages Editor
|
|
|
|
---
|
|
|
|
## QA Results
|
|
|
|
### Review Date: 2025-12-28
|
|
|
|
### Reviewed By: Quinn (Test Architect)
|
|
|
|
### Code Quality Assessment
|
|
|
|
The implementation demonstrates **excellent code quality** overall. The developer has:
|
|
|
|
1. **Followed existing patterns** - Class-based Volt components match the project architecture
|
|
2. **Proper separation of concerns** - Model with localized accessors, clean Livewire components
|
|
3. **Security-first approach** - HTML sanitization via `mews/purifier` package with `clean()` helper
|
|
4. **Comprehensive test coverage** - 29 tests covering all acceptance criteria
|
|
5. **Full bilingual support** - Arabic/English translations complete in both `pages.php` and `navigation.php`
|
|
6. **AdminLog integration** - Page updates are properly audited
|
|
|
|
**Highlights:**
|
|
- The `Page` model's `getTitle()` and `getContent()` methods with locale fallback are well-designed
|
|
- Quill.js integration with RTL support and dark mode styling is thorough
|
|
- Legacy route redirects maintain backward compatibility
|
|
|
|
### Refactoring Performed
|
|
|
|
No refactoring was required. The code meets all quality standards.
|
|
|
|
### Compliance Check
|
|
|
|
- Coding Standards: ✓ All code passes Pint formatting
|
|
- Project Structure: ✓ Files in correct locations following project conventions
|
|
- Testing Strategy: ✓ Feature tests cover all acceptance criteria with appropriate Volt::test() usage
|
|
- All ACs Met: ✓ All 26 acceptance criteria verified as implemented
|
|
|
|
### Improvements Checklist
|
|
|
|
All items are satisfied - no changes required:
|
|
|
|
- [x] Page model with localized accessors
|
|
- [x] Migration with proper schema (longText for content)
|
|
- [x] PageSeeder using firstOrCreate for idempotency
|
|
- [x] PageFactory with terms() and privacy() states
|
|
- [x] Admin index page lists both legal pages
|
|
- [x] Admin edit page with Quill.js rich text editor
|
|
- [x] Language tabs (Arabic/English) working
|
|
- [x] Preview modal shows sanitized content
|
|
- [x] RTL support for Arabic content in editor
|
|
- [x] Dark mode styling for Quill editor
|
|
- [x] Public legal page view with locale-based content
|
|
- [x] Last updated timestamp displayed
|
|
- [x] HTML sanitization via mews/purifier
|
|
- [x] AdminLog created on page save
|
|
- [x] Legacy routes redirect to new dynamic routes
|
|
- [x] Sidebar navigation includes Legal Pages link
|
|
- [x] All translations complete (en/ar)
|
|
- [x] 29 comprehensive feature tests
|
|
|
|
### Security Review
|
|
|
|
**Status: PASS**
|
|
|
|
- XSS Prevention: HTML content is sanitized using `clean()` (mews/purifier) both on save and on display
|
|
- Authorization: Admin-only routes properly protected by `admin` middleware
|
|
- CSRF: Standard Laravel/Livewire CSRF protection in place
|
|
- Input Validation: Slug constrained to `terms|privacy` pattern in routes
|
|
|
|
### Performance Considerations
|
|
|
|
**Status: PASS**
|
|
|
|
- Database: Uses `longText` for content columns (appropriate for legal documents)
|
|
- Queries: Simple single-row queries by slug with unique index
|
|
- CDN Assets: Quill.js loaded from CDN (consider bundling for production)
|
|
- No N+1 issues detected
|
|
|
|
### Files Modified During Review
|
|
|
|
None - no modifications were required.
|
|
|
|
### Gate Status
|
|
|
|
Gate: PASS → docs/qa/gates/6.9-legal-pages-editor.yml
|
|
|
|
### Recommended Status
|
|
|
|
✓ Ready for Done
|
|
|
|
The implementation fully satisfies all acceptance criteria with comprehensive test coverage, proper security measures, and clean code following project conventions
|