Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions GDPR_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# GDPR PII Export & Delete Workflow

This implementation provides full GDPR compliance for FinMind users, enabling them to exercise their Right to Access and Right to be Forgotten.

## Features

### 1. Data Export (`/api/gdpr/export`)
- **ZIP Download**: Complete user data export as a downloadable ZIP file
- **Structured JSON**: All data exported in clean, readable JSON format
- **Comprehensive Coverage**: Exports all user-related data across all tables
- **Export Preview**: Preview what will be exported before downloading

### 2. Data Deletion (`/api/gdpr/delete`)
- **Right to be Forgotten**: Permanently delete all user PII
- **Safety Confirmation**: Requires explicit confirmation token
- **Cascade Deletion**: Properly handles foreign key relationships
- **Deletion Preview**: See exactly what will be deleted
- **Irreversible**: Once deleted, data cannot be recovered

### 3. Audit Trail
- **Complete Logging**: All GDPR actions logged with timestamps
- **Non-Repudiation**: Cannot deny or dispute logged actions
- **Compliance Ready**: Meets regulatory audit requirements

## API Endpoints

### Public
- `GET /api/gdpr/info` - Information about GDPR rights

### Authenticated (requires Bearer token)
- `GET /api/gdpr/export` - Download all personal data (ZIP)
- `GET /api/gdpr/export/preview` - Preview export contents
- `POST /api/gdpr/delete` - Delete all personal data
- `GET /api/gdpr/delete/preview` - Preview deletion impact
- `GET /api/gdpr/audit-log` - View GDPR audit history

## Usage Examples

### Export Your Data
```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://api.finmind.app/gdpr/export \
--output my_data.zip
```

### Preview Before Export
```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://api.finmind.app/gdpr/export/preview
```

### Delete Your Account
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"confirmation_token":"DELETE_MY_DATA_PERMANENTLY"}' \
https://api.finmind.app/gdpr/delete
```

### Preview Before Deletion
```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://api.finmind.app/gdpr/delete/preview
```

## Data Exported

The export includes:
- User profile (email, currency preference, role)
- Categories
- Expenses (regular and recurring)
- Bills and reminders
- Ad impressions
- Subscription history
- Audit logs

## Security Considerations

1. **Authentication Required**: All data access requires valid JWT token
2. **Confirmation Token**: Deletion requires explicit confirmation to prevent accidents
3. **Audit Logging**: All actions logged for compliance
4. **Cascade Delete**: Related data properly cleaned up
5. **Anonymized Audit**: Post-deletion, audit logs retain action record but anonymize user_id

## Testing

Run the GDPR test suite:
```bash
cd packages/backend
pytest tests/test_gdpr.py -v
```

Tests cover:
- Export functionality
- Deletion with confirmation
- Audit logging
- Preview endpoints
- Full integration workflow

## GDPR Compliance Notes

### Right to Access (Article 15)
✅ Users can download all their personal data in a structured format

### Right to Erasure (Article 17)
✅ Users can permanently delete their account and all data

### Data Portability (Article 20)
✅ Data exported in machine-readable JSON format

### Records of Processing (Article 30)
✅ All GDPR actions logged with audit trail

## Implementation Details

### Files Added/Modified

**New Files:**
- `packages/backend/app/routes/gdpr.py` - Main GDPR route handlers
- `packages/backend/tests/test_gdpr.py` - Comprehensive test suite

**Modified Files:**
- `packages/backend/app/routes/__init__.py` - Register GDPR blueprint
- `packages/backend/app/routes/auth.py` - Add `verify_token` helper
- `packages/backend/app/openapi.yaml` - API documentation

### Database Impact

No schema changes required. Uses existing tables:
- `users` - User account data
- `categories` - User categories
- `expenses` - User expenses
- `recurring_expenses` - Recurring expense definitions
- `bills` - User bills
- `reminders` - User reminders
- `ad_impressions` - Ad interaction data
- `user_subscriptions` - Subscription data
- `audit_logs` - Action audit trail

### Deletion Order

To respect foreign key constraints:
1. Expenses (child of users, categories)
2. RecurringExpenses (child of users, categories)
3. Reminders (child of users, bills)
4. Bills (child of users)
5. Categories (child of users)
6. AdImpressions (child of users)
7. UserSubscriptions (child of users)
8. AuditLogs (anonymized, not deleted)
9. Users (final deletion)

## Acceptance Criteria Met

- [x] Export package generation (ZIP with JSON)
- [x] Irreversible deletion workflow
- [x] Audit trail logging (all actions logged)
- [x] Production-ready implementation
- [x] Comprehensive test coverage
- [x] Documentation updated (OpenAPI spec)

## Bounty Information

This implementation addresses FinMind Issue #76:
**PII Export & Delete Workflow (GDPR-ready) - $500 Bounty**

- Implements complete GDPR compliance features
- Production-ready code with error handling
- Full test coverage
- API documentation included
167 changes: 167 additions & 0 deletions packages/backend/app/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ tags:
- name: Bills
- name: Reminders
- name: Insights
- name: GDPR
paths:
/auth/register:
post:
Expand Down Expand Up @@ -481,6 +482,172 @@ paths:
application/json:
schema: { $ref: '#/components/schemas/Error' }

/gdpr/info:
get:
summary: Get GDPR rights information
tags: [GDPR]
description: Public endpoint with information about GDPR rights and available endpoints
responses:
'200':
description: GDPR information
content:
application/json:
schema:
type: object
properties:
service: { type: string }
gdpr_compliance: { type: object }
data_retention: { type: object }

/gdpr/export:
get:
summary: Export all personal data (GDPR Right to Access)
tags: [GDPR]
security: [{ bearerAuth: [] }]
description: Downloads a ZIP file containing all user PII data
responses:
'200':
description: ZIP file with user data
content:
application/zip:
schema:
type: string
format: binary
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }

/gdpr/export/preview:
get:
summary: Preview data to be exported
tags: [GDPR]
security: [{ bearerAuth: [] }]
description: Returns a summary of what data will be exported
responses:
'200':
description: Export preview
content:
application/json:
schema:
type: object
properties:
preview_generated_at: { type: string, format: date-time }
data_categories:
type: object
properties:
profile: { type: boolean }
categories: { type: integer }
expenses: { type: integer }
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }

/gdpr/delete:
post:
summary: Delete all personal data (GDPR Right to be Forgotten)
tags: [GDPR]
security: [{ bearerAuth: [] }]
description: Permanently deletes all user data. Requires confirmation token.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [confirmation_token]
properties:
confirmation_token:
type: string
description: Must be 'DELETE_MY_DATA_PERMANENTLY'
example:
confirmation_token: DELETE_MY_DATA_PERMANENTLY
responses:
'200':
description: Data deleted successfully
content:
application/json:
schema:
type: object
properties:
message: { type: string }
deleted_user_email: { type: string }
deletion_completed_at: { type: string, format: date-time }
audit_log_id: { type: integer }
'400':
description: Invalid confirmation token
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }

/gdpr/delete/preview:
get:
summary: Preview data to be deleted
tags: [GDPR]
security: [{ bearerAuth: [] }]
description: Returns a summary of what data will be deleted
responses:
'200':
description: Deletion preview
content:
application/json:
schema:
type: object
properties:
warning: { type: string }
user_email: { type: string }
data_to_be_deleted:
type: object
properties:
profile: { type: boolean }
categories_count: { type: integer }
expenses_count: { type: integer }
required_confirmation: { type: string }
consequences:
type: array
items: { type: string }
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }

/gdpr/audit-log:
get:
summary: Get GDPR audit log
tags: [GDPR]
security: [{ bearerAuth: [] }]
description: Returns audit trail of GDPR-related actions
responses:
'200':
description: Audit log entries
content:
application/json:
schema:
type: object
properties:
audit_entries:
type: array
items:
type: object
properties:
id: { type: integer }
action: { type: string }
created_at: { type: string, format: date-time }
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }

components:
securitySchemes:
bearerAuth:
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/app/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .categories import bp as categories_bp
from .docs import bp as docs_bp
from .dashboard import bp as dashboard_bp
from .gdpr import bp as gdpr_bp


def register_routes(app: Flask):
Expand All @@ -18,3 +19,4 @@ def register_routes(app: Flask):
app.register_blueprint(categories_bp, url_prefix="/categories")
app.register_blueprint(docs_bp, url_prefix="/docs")
app.register_blueprint(dashboard_bp, url_prefix="/dashboard")
app.register_blueprint(gdpr_bp, url_prefix="/gdpr")
22 changes: 22 additions & 0 deletions packages/backend/app/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,25 @@ def _store_refresh_session(refresh_token: str, uid: str):
return
ttl = max(int(exp - time.time()), 1)
redis_client.setex(_refresh_key(jti), ttl, uid)


def verify_token(token: str) -> int | None:
"""Verify a JWT access token and return user_id if valid.

Used by GDPR endpoints for token verification.

Args:
token: JWT access token string

Returns:
User ID if token is valid, None otherwise
"""
try:
from flask_jwt_extended import decode_token
payload = decode_token(token)
user_id = payload.get("sub")
if user_id:
return int(user_id)
except Exception:
return None
return None
Loading