+
+
+
Privacy & Data
+
+ Manage your personal data. Export everything or permanently delete your account.
+
+
+
+
+ {/* Export Section */}
+
+
+
+
+
+
+
Your Data
+
+ Download a complete copy of all your data as JSON.
+
+
+
+
+ Your export will include: profile information, expenses, bills, reminders,
+ categories, accounts, savings goals, subscriptions, and activity logs.
+ The download link expires after 24 hours.
+
+
+
+
+
+
+ {/* Delete Account Section */}
+
+
+
+
+
+
+
Delete Account
+
+ Permanently delete your account and all associated data.
+
+
+
+
+
+
Warning: This action is irreversible.
+
Deleting your account will permanently remove:
+
+ - Your profile and login credentials
+ - All expenses and transaction history
+ - All bills and reminders
+ - All categories and accounts
+ - All savings goals and contributions
+ - All subscription data
+
+
We recommend exporting your data before proceeding.
+
+
+ {deleteStep === 'idle' && (
+
+
+
+ )}
+
+ {deleteStep === 'confirming' && (
+
+
+ Preparing deletion...
+
+ )}
+
+ {deleteStep === 'typing' && (
+
+
+ Type DELETE to confirm account deletion:
+
+
setDeleteConfirmText(e.target.value)}
+ autoFocus
+ />
+
+
+
+
+
+ )}
+
+ {deleteStep === 'deleting' && (
+
+
+ Deleting your account...
+
+ )}
+
+
+ {/* Past Requests */}
+
+
+
+
+
+
+
Request History
+
+ Past data export and deletion requests.
+
+
+
+
+ {loadingRequests ? (
+
Loading requests...
+ ) : requests.length === 0 ? (
+
+ No data requests yet.
+
+ ) : (
+
+ {requests.map((req) => (
+
+
+ {statusIcon(req.status)}
+
+
+ {req.request_type === 'EXPORT' ? 'Data Export' : 'Account Deletion'}
+
+
+ {req.created_at
+ ? new Date(req.created_at).toLocaleDateString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ })
+ : 'Unknown date'}
+ {' '}·{' '}{req.status}
+
+
+
+ {req.has_download && req.status === 'COMPLETED' && (
+
+ )}
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/packages/backend/app/db/schema.sql b/packages/backend/app/db/schema.sql
index 410189def..077bb9c06 100644
--- a/packages/backend/app/db/schema.sql
+++ b/packages/backend/app/db/schema.sql
@@ -121,5 +121,21 @@ CREATE TABLE IF NOT EXISTS audit_logs (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id) ON DELETE SET NULL,
action VARCHAR(100) NOT NULL,
+ details TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
+
+ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS details TEXT;
+
+-- GDPR data requests
+CREATE TABLE IF NOT EXISTS data_requests (
+ id SERIAL PRIMARY KEY,
+ user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ request_type VARCHAR(20) NOT NULL,
+ status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
+ download_url TEXT,
+ expires_at TIMESTAMP,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ completed_at TIMESTAMP
+);
+CREATE INDEX IF NOT EXISTS idx_data_requests_user ON data_requests(user_id, created_at DESC);
diff --git a/packages/backend/app/models.py b/packages/backend/app/models.py
index 64d448104..7ea0285a3 100644
--- a/packages/backend/app/models.py
+++ b/packages/backend/app/models.py
@@ -127,9 +127,85 @@ class UserSubscription(db.Model):
started_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+class DataRequestType(str, Enum):
+ EXPORT = "EXPORT"
+ DELETE = "DELETE"
+
+
+class DataRequestStatus(str, Enum):
+ PENDING = "PENDING"
+ PROCESSING = "PROCESSING"
+ COMPLETED = "COMPLETED"
+ FAILED = "FAILED"
+
+
+class DataRequest(db.Model):
+ __tablename__ = "data_requests"
+ id = db.Column(db.Integer, primary_key=True)
+ user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
+ request_type = db.Column(db.String(20), nullable=False)
+ status = db.Column(db.String(20), nullable=False, default=DataRequestStatus.PENDING.value)
+ download_url = db.Column(db.Text, nullable=True)
+ expires_at = db.Column(db.DateTime, nullable=True)
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+ completed_at = db.Column(db.DateTime, nullable=True)
+
+
class AuditLog(db.Model):
__tablename__ = "audit_logs"
id = db.Column(db.Integer, primary_key=True)
- user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
+ user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
action = db.Column(db.String(100), nullable=False)
+ details = db.Column(db.Text, nullable=True)
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+
+
+class SavingsGoal(db.Model):
+ __tablename__ = "savings_goals"
+ id = db.Column(db.Integer, primary_key=True)
+ user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
+ name = db.Column(db.String(200), nullable=False)
+ description = db.Column(db.String(500), nullable=True)
+ target_amount = db.Column(db.Numeric(12, 2), nullable=False)
+ current_amount = db.Column(db.Numeric(12, 2), default=0, nullable=False)
+ currency = db.Column(db.String(10), default="INR", nullable=False)
+ deadline = db.Column(db.Date, nullable=True)
+ icon = db.Column(db.String(50), nullable=True)
+ color = db.Column(db.String(20), nullable=True)
+ is_completed = db.Column(db.Boolean, default=False, nullable=False)
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+ updated_at = db.Column(
+ db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
+ )
+
+ milestones = db.relationship(
+ "GoalMilestone", backref="goal", lazy="dynamic",
+ cascade="all, delete-orphan"
+ )
+ contributions = db.relationship(
+ "GoalContribution", backref="goal", lazy="dynamic",
+ cascade="all, delete-orphan"
+ )
+
+
+class GoalMilestone(db.Model):
+ __tablename__ = "goal_milestones"
+ id = db.Column(db.Integer, primary_key=True)
+ goal_id = db.Column(
+ db.Integer, db.ForeignKey("savings_goals.id", ondelete="CASCADE"), nullable=False
+ )
+ name = db.Column(db.String(100), nullable=False)
+ target_percentage = db.Column(db.Integer, nullable=False)
+ reached_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+
+
+class GoalContribution(db.Model):
+ __tablename__ = "goal_contributions"
+ id = db.Column(db.Integer, primary_key=True)
+ goal_id = db.Column(
+ db.Integer, db.ForeignKey("savings_goals.id", ondelete="CASCADE"), nullable=False
+ )
+ amount = db.Column(db.Numeric(12, 2), nullable=False)
+ note = db.Column(db.String(500), nullable=True)
+ contributed_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
diff --git a/packages/backend/app/routes/__init__.py b/packages/backend/app/routes/__init__.py
index f13b0f897..cf2fe0bfc 100644
--- a/packages/backend/app/routes/__init__.py
+++ b/packages/backend/app/routes/__init__.py
@@ -7,6 +7,8 @@
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
+from .savings import bp as savings_bp
def register_routes(app: Flask):
@@ -18,3 +20,5 @@ 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="/privacy")
+ app.register_blueprint(savings_bp, url_prefix="/savings")
diff --git a/packages/backend/app/routes/gdpr.py b/packages/backend/app/routes/gdpr.py
new file mode 100644
index 000000000..c32087990
--- /dev/null
+++ b/packages/backend/app/routes/gdpr.py
@@ -0,0 +1,130 @@
+"""GDPR privacy routes — data export & account deletion."""
+
+import json
+from flask import Blueprint, jsonify, request, Response
+from flask_jwt_extended import jwt_required, get_jwt_identity
+from ..services.gdpr import (
+ confirm_data_deletion,
+ generate_export_package,
+ get_request_status,
+ get_user_requests,
+ request_data_deletion,
+ request_data_export,
+)
+from ..models import DataRequest, DataRequestStatus, DataRequestType
+from ..extensions import db
+from datetime import datetime
+import logging
+
+bp = Blueprint("privacy", __name__)
+logger = logging.getLogger("finmind.privacy")
+
+
+@bp.post("/export")
+@jwt_required()
+def create_export():
+ """Request a data export package."""
+ uid = int(get_jwt_identity())
+ try:
+ req = request_data_export(uid)
+ logger.info("Export request created user_id=%s request_id=%s", uid, req.id)
+ return jsonify(
+ request_id=req.id,
+ status=req.status,
+ message="Export ready for download",
+ ), 201
+ except Exception as exc:
+ logger.exception("Export request failed user_id=%s", uid)
+ return jsonify(error=f"Export failed: {exc}"), 500
+
+
+@bp.get("/export/