|
96 | 96 | - `app.py` - Main Flask application with all routes |
97 | 97 | - `models.py` - SQLAlchemy models (Task, Checklist, RecurringSeries, etc.) |
98 | 98 | - `extensions.py` - Flask extensions (db) |
| 99 | +- `utils/` - **NEW**: Utility functions for query scoping, security, helpers |
99 | 100 | - `templates/` - Jinja2 HTML templates |
100 | 101 | - `templates/partials/` - Reusable template components |
101 | 102 | - `static/` - CSS, JS, and assets |
@@ -275,6 +276,79 @@ checklist = Checklist.query.get_or_404(checklist_id) |
275 | 276 | - `migrations/archive/add_user_system.py` - Adds user system, creates default Hunter account, assigns existing data to Hunter (ID=1) |
276 | 277 | - `migrations/archive/add_account_uuid_and_history.py` - Adds account_uuid field, AccountHistory table, generates UUIDs for existing users, sets default display names |
277 | 278 |
|
| 279 | +## Query Scoping & Data Security - CRITICAL |
| 280 | +**Centralized user data isolation to prevent data leaks** |
| 281 | + |
| 282 | +### Architecture |
| 283 | +- **UserScopedMixin** added to all user-owned models (Task, Checklist, RecurringSeries, CalendarSource) |
| 284 | +- **Auto-injection event listener** sets user_id automatically on create |
| 285 | +- **Helper functions** for shared calendar access and permission checks |
| 286 | +- 📚 [Data Security Documentation](docs/systems/data-security.md) - comprehensive security guide |
| 287 | + |
| 288 | +### UserScopedMixin - Automatic User Filtering |
| 289 | +**✅ PREFERRED: Use mixin methods for all user-scoped queries** |
| 290 | + |
| 291 | +```python |
| 292 | +# ✅ CORRECT - Uses mixin for automatic user_id filtering |
| 293 | +task = Task.get_for_user(task_id) # Auto-filters by current_user.id |
| 294 | +tasks = Task.all_for_user(checklist_id=5) # Auto-injects user_id filter |
| 295 | +count = Task.count_for_user(checklist_id=5) # Count with user filter |
| 296 | +exists = Task.exists_for_user(task_id) # Check existence for user |
| 297 | + |
| 298 | +# ❌ DEPRECATED - Manual filtering (error-prone, use mixin instead) |
| 299 | +task = Task.query.filter_by(id=task_id, user_id=current_user.id).first() |
| 300 | +``` |
| 301 | + |
| 302 | +**Why this matters:** |
| 303 | +- Prevents forgetting user_id filter (92+ manual checks eliminated) |
| 304 | +- Makes secure behavior the default |
| 305 | +- Easier to audit - consistent pattern everywhere |
| 306 | +- Prevents accidental data leaks |
| 307 | + |
| 308 | +### Auto-Injection on Create |
| 309 | +**user_id automatically set when creating new objects** |
| 310 | + |
| 311 | +```python |
| 312 | +# ✅ CORRECT - user_id auto-injected by event listener |
| 313 | +task = Task(title="Test task", checklist_id=5) |
| 314 | +db.session.add(task) |
| 315 | +db.session.commit() |
| 316 | +# task.user_id automatically set to current_user.id before flush |
| 317 | + |
| 318 | +# ❌ OLD WAY - Manual assignment (still works but unnecessary) |
| 319 | +task = Task(title="Test task", user_id=current_user.id, checklist_id=5) |
| 320 | +``` |
| 321 | + |
| 322 | +**Implementation:** SQLAlchemy `before_flush` event listener in `utils/query_scoping.py` |
| 323 | + |
| 324 | +### Shared Calendar Access |
| 325 | +**Helper functions for accessing shared content** |
| 326 | + |
| 327 | +```python |
| 328 | +# Get accessible shared calendar IDs (owned + member) |
| 329 | +accessible_ids = get_accessible_shared_calendar_ids() # Request-scoped cache |
| 330 | + |
| 331 | +# Check if user can access object (ownership OR shared calendar) |
| 332 | +if not check_user_can_access(task): |
| 333 | + abort(403) |
| 334 | + |
| 335 | +# Require ownership or shared access (raises 403 if denied) |
| 336 | +require_ownership_or_shared_access(task, permission='edit') |
| 337 | +``` |
| 338 | + |
| 339 | +### Critical Security Rules |
| 340 | +1. **✅ ALWAYS use mixin methods** - `.get_for_user()`, `.all_for_user()` for user-scoped queries |
| 341 | +2. **✅ Auto-injection active** - Don't manually set user_id on create (let event listener handle it) |
| 342 | +3. **✅ Include shared calendars** - Use `get_accessible_shared_calendar_ids()` where appropriate |
| 343 | +4. **✅ Check permissions** - Use `check_user_can_access()` before modifying objects |
| 344 | +5. **❌ NEVER use .get() or .get_or_404()** directly - no user isolation |
| 345 | +6. **❌ Return 403 Forbidden** - Not 404 when object exists but user doesn't own it |
| 346 | + |
| 347 | +### Migration Notes |
| 348 | +- `RecurrenceRule` model removed (deprecated after RRULE migration) |
| 349 | +- `migrations/archive/drop_recurrence_rules_table.py` - Drops deprecated table |
| 350 | +- All recurrence patterns now use RFC5545 RRULE standard exclusively |
| 351 | + |
278 | 352 | ## AJAX Architecture - CRITICAL |
279 | 353 | **Three AJAX endpoints for dynamic updates without page reloads** |
280 | 354 |
|
@@ -973,8 +1047,8 @@ existing_room = db.session.query(ChatRoom).filter( |
973 | 1047 | **CSS Variable-based theme system for scalable, maintainable theming** |
974 | 1048 |
|
975 | 1049 | ### Architecture |
976 | | -- **Two themes**: Dark (default) and Light |
977 | | -- Theme stored in User model: `current_user.theme` (database column) |
| 1050 | +- **Nine themes available**: Dark (default), Light, Ocean Blue, Forest Green, Sunset Purple, Warm Amber, Colorful, Slate Grey, Charcoal Grey |
| 1051 | +- Theme stored in User model: `current_user.theme` (database column, max 10 chars) |
978 | 1052 | - Body tag gets theme class: `<body class="theme-{{ current_user.theme if current_user.is_authenticated else 'dark' }}">` |
979 | 1053 | - **CSS Custom Properties (Variables)** define all colors - NO hardcoded hex values in styles |
980 | 1054 | - Each theme defines variable values at `:root` level - NO `.theme-light` override blocks scattered throughout |
@@ -1052,15 +1126,34 @@ existing_room = db.session.query(ChatRoom).filter( |
1052 | 1126 | .btn { color: var(--text-link); } |
1053 | 1127 | ``` |
1054 | 1128 |
|
1055 | | -### Adding New Themes (Future) |
1056 | | -```css |
1057 | | -.theme-high-contrast { |
1058 | | - --bg-primary: #000000; |
1059 | | - --text-primary: #ffffff; |
1060 | | - /* ... define all variables */ |
1061 | | -} |
| 1129 | +### Backend Validation |
| 1130 | +**Theme values validated in settings_update() route:** |
| 1131 | +```python |
| 1132 | +VALID_THEMES = ['dark', 'light', 'ocean', 'forest', 'sunset', 'amber', 'colorful', 'slate', 'charcoal'] |
| 1133 | +theme = request.form.get('theme', 'dark') |
| 1134 | + |
| 1135 | +if theme not in VALID_THEMES: |
| 1136 | + flash(f"Invalid theme: {theme}", "danger") |
| 1137 | + return redirect(url_for('settings_view')) |
1062 | 1138 | ``` |
1063 | 1139 |
|
| 1140 | +### Theme Descriptions |
| 1141 | +- **Dark** (default) - Deep blue-grey professional theme |
| 1142 | +- **Light** - Clean white theme for bright environments |
| 1143 | +- **Ocean Blue** - Deep blue professional with cooler tones |
| 1144 | +- **Forest Green** - Green-accented theme with natural feel |
| 1145 | +- **Sunset Purple** - Purple-based theme with warm accents |
| 1146 | +- **Warm Amber** - Orange/amber theme with cozy atmosphere |
| 1147 | +- **Colorful** - Vibrant rainbow theme with gradient buttons |
| 1148 | +- **Slate Grey** - Light grey professional theme |
| 1149 | +- **Charcoal Grey** - Dark grey theme with muted tones |
| 1150 | + |
| 1151 | +### Adding New Themes (Future) |
| 1152 | +1. Define all CSS variables in `static/styles.css` |
| 1153 | +2. Add theme value to `VALID_THEMES` list in `app.py` settings_update() |
| 1154 | +3. Add theme card to Settings page with color preview |
| 1155 | +4. Ensure all variable names match existing themes (100+ variables) |
| 1156 | + |
1064 | 1157 | ### Accessibility |
1065 | 1158 | - Ensure WCAG AA contrast in all themes (4.5:1 minimum) |
1066 | 1159 | - Test with color blindness simulators |
|
0 commit comments