Privacy settings #60

Merged
Ichitux merged 12 commits from privacy_settings into main 2026-08-27 06:23:54 +00:00
Showing only changes of commit fba80ae894 - Show all commits
@@ -0,0 +1,316 @@
# Design: Cookies Banner & Privacy Page
## Overview
Add GDPR/RGPD-compliant cookie consent management, a health data consent flow, and a privacy policy page to FarmaFinder across both web and mobile platforms. The app collects significant personal data including health/medical data (TSI card scanning), requiring special category consent under GDPR Article 9.
**Company/Data Controller:** Hacecalor S.L.
**Languages:** Spanish (primary) + Catalan
**Platforms:** Web frontend (apps/frontend) + Mobile frontend (apps/frontend-mobile)
---
## Consent Categories
| Category | Required | Default | Description |
|---|---|---|---|
| **essential** | Yes (always on) | ON | Session cookie (express-session, HTTP-only), CSRF, authentication |
| **analytics** | No (opt-in) | OFF | Grafana Faro (browser RUM, Web Vitals, error tracking) |
| **preferences** | No (opt-in) | OFF | Theme (dark/light), language (es/ca), saved searches |
| **health_data** | No (opt-in) | OFF | TSI card scanning, CIP code extraction, prescription lookup |
**health_data consent** is shown in two places:
1. In the cookie banner (as a 4th toggle, like other categories)
2. Re-prompted via a dedicated modal when the user first attempts to scan their TSI card
If the user already accepted health_data via the banner, the TSI modal is skipped.
---
## Architecture
**Approach:** Server-side consent + client sync
- Consent stored in database (`user_consents` table) for logged-in users
- Consent cached in `localStorage` (web) or `expo-secure-store` (mobile) for fast access and anonymous users
- On login/register: Anonymous consents migrated to the user account
- Backend can enforce consent via middleware
---
## Database Schema
### New table: `user_consents`
```sql
CREATE TABLE user_consents (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
session_id VARCHAR(255),
category VARCHAR(20) NOT NULL CHECK (category IN ('essential', 'analytics', 'preferences', 'health_data')),
granted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, category),
UNIQUE(session_id, category)
);
CREATE INDEX idx_user_consents_user_id ON user_consents(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX idx_user_consents_session_id ON user_consents(session_id) WHERE session_id IS NOT NULL;
```
- `user_id` is NULL for anonymous users (consent tied to session)
- `session_id` is NULL for logged-in users (consent tied to account)
- `essential` category is always forced to `true` by the backend
---
## Backend API
### Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| `GET` | `/api/consents` | Optional | Get current user's/session's consents |
| `PUT` | `/api/consents` | Optional | Save consent preferences |
#### GET /api/consents
Response:
```json
{
"essential": true,
"analytics": false,
"preferences": true,
"health_data": false
}
```
#### PUT /api/consents
Request:
```json
{
"categories": {
"analytics": true,
"preferences": false,
"health_data": true
}
}
```
Response: Same as GET (full consent state after save).
- `essential` is always forced to `true` in the response, regardless of what is sent
- For logged-in users: saved with `user_id`
- For anonymous users: saved with `session_id` from express-session
### Consent Migration on Login/Register
When a user logs in or registers:
1. Fetch any consents stored under the current `session_id`
2. Merge with any existing `user_id` consents (user_id takes precedence for conflicts)
3. Delete session-based consents
4. Return merged consents
### Middleware
`requireConsent(category)` — checks if the current user/session has granted consent for the given category. Returns 403 if not granted.
Applied to:
- `POST /api/tsi/scan` — requires `health_data`
---
## Web Frontend (apps/frontend)
### New Components
#### CookieBanner.jsx + CookieBanner.css
- Fixed banner at bottom of screen, shown on first visit (no consent record in localStorage)
- Brief explanation text
- 4 category toggles: Esenciales (locked ON), Analítica, Preferencias, Datos de Salud
- Buttons: "Aceptar todo" / "Rechazar opcionales" / "Guardar"
- "Más información" link → opens PrivacyView
- Styled with existing CSS custom properties (Material Design 3 tokens)
- Responsive: full-width on mobile, centered card on desktop
#### HealthConsentModal.jsx + HealthConsentModal.css
- Modal shown when user first taps TSI scanner (if health_data consent not yet granted)
- Explains what data is collected (CIP code, prescriptions) and how it's used
- Buttons: "Aceptar y escanear" / "Cancelar"
- Sets health_data consent on accept, then proceeds to scan
- Skipped if user previously accepted via banner
#### PrivacyView.jsx + PrivacyView.css
- Full privacy policy page, new screen in SPA routing
- Sections: Data Controller, Data Collected, Purpose, Legal Basis, External Services, Retention, User Rights, Contact, Cookie Policy
- Content in Spanish with Catalan toggle (using existing i18n pattern)
- Accessible via route `privacy`
#### Footer / BottomNav update
- Adds "Política de privacidad" link to existing navigation
- Opens PrivacyView
### Consent Sync Utility
- `utils/consent.js` — manages localStorage ↔ API sync
- On app load: fetch from API if logged in, else read localStorage
- On consent change: update localStorage + call PUT /api/consents
- Grafana Faro initialization gated on analytics consent
### Grafana Faro Gating
In `utils/faro.js`:
- Check consent before calling `init()`
- If analytics consent not granted, Faro is not initialized
- If consent is granted later (via banner), re-initialize Faro
### TSI Scanner Gating
In `ScannerView.jsx`:
- Before opening TSI scan, check health_data consent
- If not granted, show HealthConsentModal
- If granted, proceed directly to scan
---
## Mobile Frontend (apps/frontend-mobile)
### New Components
#### components/CookieBanner.tsx
- Bottom sheet / slide-up panel with same 4 categories
- Same toggle logic as web
- Buttons: "Aceptar todo" / "Rechazar opcionales" / "Guardar"
- Styled with ThemeProvider tokens (dark/light theme)
- Shown on first launch (checked via expo-secure-store key `consents_initialized`)
#### components/HealthConsentModal.tsx
- Same purpose as web: shown when user first tries to scan TSI
- Explains data collection, two buttons
- Sets consent via API, then proceeds to scanner
- Skipped if consent already granted
#### app/privacy.tsx (or app/(tabs)/privacy.tsx)
- New Expo Router screen for privacy policy
- Same content structure as web
- Uses ScrollView for long content
- Accessible from profile screen
### Consent Sync Utility
- `services/consent.ts` — manages expo-secure-store ↔ API sync
- On app load: fetch from API if logged in, else read secure-store
- On consent change: update secure-store + call PUT /api/consents
### API Client Additions
- `services/api.ts` gets: `getConsents()`, `updateConsents(categories)`
- Same endpoints as web
### TSI Scanner Gating
In scanner flow:
- Before opening TSI scan, check health_data consent
- If not granted, show HealthConsentModal
- If granted, proceed directly
---
## i18n Keys
### Cookie Banner
- `cookie_banner.title`
- `cookie_banner.description`
- `cookie_banner.category.essential`
- `cookie_banner.category.essential_desc`
- `cookie_banner.category.analytics`
- `cookie_banner.category.analytics_desc`
- `cookie_banner.category.preferences`
- `cookie_banner.category.preferences_desc`
- `cookie_banner.category.health_data`
- `cookie_banner.category.health_data_desc`
- `cookie_banner.accept_all`
- `cookie_banner.reject_optional`
- `cookie_banner.save`
- `cookie_banner.more_info`
### Health Consent Modal
- `health_consent.title`
- `health_consent.description`
- `health_consent.accept`
- `health_consent.cancel`
### Privacy Page
- `privacy.title`
- `privacy.section.*` (all 9 sections with headings and content)
### Navigation
- `nav.privacy`
---
## Privacy Policy Content (9 Sections)
1. **Data Controller** — Hacecalor S.L., contact info
2. **Data Collected** — User profiles (username, email, name, city, avatar), addresses, geolocation, search history, health data (TSI/CIP codes, prescriptions), push notification tokens, session data
3. **Purpose of Processing** — Medicine search, pharmacy locator, availability alerts, health card scanning, app functionality
4. **Legal Basis** — Consent (health data, analytics), legitimate interest (security, fraud prevention), contract (account services)
5. **External Services** — CIMA API (Spanish Medicines Agency), Grafana Faro (analytics), Nominatim/OpenStreetMap (geocoding), N8N (automation), email provider (password reset)
6. **Data Retention** — Session: 24h, Account: until deletion, Consent records: 3 years, Search history: 6 months
7. **User Rights** — Access, rectification, erasure, portability, objection, withdraw consent
8. **Contact** — How to exercise rights / contact the data controller
9. **Cookie Policy** — List of cookies by category, purpose, duration
---
## Implementation Phases
### Phase 1: Backend + Consent Infrastructure
- Create `user_consents` DB migration (PostgreSQL + SQLite)
- Add `GET/PUT /api/consents` endpoints
- Add `requireConsent()` middleware
- Add consent migration on login/register
- **Deliverable**: Consent API working
### Phase 2: Web Frontend
- CookieBanner.jsx + CSS
- HealthConsentModal.jsx + CSS
- PrivacyView.jsx + CSS
- Footer/nav link to privacy page
- Consent sync utility (localStorage ↔ API)
- Grafana Faro gated on analytics consent
- TSI scanner gated on health_data consent
- i18n keys (es + ca)
- **Deliverable**: Full web consent flow working
### Phase 3: Mobile Frontend
- CookieBanner.tsx
- HealthConsentModal.tsx
- privacy.tsx screen
- Consent sync utility (expo-secure-store ↔ API)
- API client methods (getConsents, updateConsents)
- TSI scanner gated on health_data consent
- i18n keys (es + ca)
- **Deliverable**: Full mobile consent flow working
### Final: Privacy Policy Content
- Write full Spanish text for all 9 sections
- Write full Catalan translation
- Add to both web and mobile i18n files
---
## Open Questions
None — all decisions finalized during brainstorming.