diff --git a/backend/src/api/routes.ts b/backend/src/api/routes.ts index 2932bf8..0465586 100644 --- a/backend/src/api/routes.ts +++ b/backend/src/api/routes.ts @@ -24,6 +24,7 @@ import type { Portfolio } from '../types/index.js' import { ok, fail } from '../utils/apiResponse.js' import { getPortfolioExport } from '../services/portfolioExportService.js' import { assetRegistryService } from '../services/assetRegistryService.js' +import { databaseService } from '../services/databaseService.js' const router = Router() const stellarService = new StellarService() @@ -54,6 +55,68 @@ router.get('/strategies', (_req: Request, res: Response) => { return ok(res, { strategies: REBALANCE_STRATEGIES }) }) +// ─── Legal consent (GDPR/CCPA) ───────────────────────────────────────────── +/** Get consent status for a user. Required before using the app. */ +router.get('/consent/status', (req: Request, res: Response) => { + try { + const userId = (req.query.userId ?? req.query.user_id) as string + if (!userId) return fail(res, 400, 'VALIDATION_ERROR', 'userId is required') + const consent = databaseService.getConsent(userId) + const accepted = databaseService.hasFullConsent(userId) + return ok(res, { + accepted, + termsAcceptedAt: consent?.termsAcceptedAt ?? null, + privacyAcceptedAt: consent?.privacyAcceptedAt ?? null, + cookieAcceptedAt: consent?.cookieAcceptedAt ?? null + }) + } catch (error) { + logger.error('[ERROR] Consent status failed', { error: getErrorObject(error) }) + return fail(res, 500, 'INTERNAL_ERROR', getErrorMessage(error)) + } +}) + +/** Record user acceptance of ToS, Privacy Policy, Cookie Policy. */ +router.post('/consent', writeRateLimiter, (req: Request, res: Response) => { + try { + const { userId, terms, privacy, cookies } = req.body ?? {} + if (!userId || typeof userId !== 'string') return fail(res, 400, 'VALIDATION_ERROR', 'userId is required') + if (typeof terms !== 'boolean' || typeof privacy !== 'boolean' || typeof cookies !== 'boolean') { + return fail(res, 400, 'VALIDATION_ERROR', 'terms, privacy, and cookies must be booleans') + } + if (!terms || !privacy || !cookies) { + return fail(res, 400, 'VALIDATION_ERROR', 'You must accept Terms of Service, Privacy Policy, and Cookie Policy') + } + const ipAddress = req.ip ?? req.socket?.remoteAddress + const userAgent = req.get('user-agent') + databaseService.recordConsent(userId, { terms, privacy, cookies, ipAddress, userAgent }) + return ok(res, { message: 'Consent recorded', accepted: true }) + } catch (error) { + logger.error('[ERROR] Record consent failed', { error: getErrorObject(error) }) + return fail(res, 500, 'INTERNAL_ERROR', getErrorMessage(error)) + } +}) + +/** GDPR: Delete all data for a user (portfolios, history, consent). Requires JWT when enabled. */ +router.delete('/user/:address/data', requireJwtWhenEnabled, writeRateLimiter, async (req: Request, res: Response) => { + try { + const address = req.params.address + const userId = req.user?.address ?? address + if (userId !== address) return fail(res, 403, 'FORBIDDEN', 'You can only delete your own data') + if (!address) return fail(res, 400, 'VALIDATION_ERROR', 'address is required') + try { + const { deleteAllRefreshTokensForUser } = await import('../db/refreshTokenDb.js') + if (typeof deleteAllRefreshTokensForUser === 'function') { + await deleteAllRefreshTokensForUser(userId) + } + } catch (_) { /* refresh token DB optional */ } + databaseService.deleteUserData(userId) + return ok(res, { message: 'Your data has been deleted' }) + } catch (error) { + logger.error('[ERROR] Delete user data failed', { error: getErrorObject(error) }) + return fail(res, 500, 'INTERNAL_ERROR', getErrorMessage(error)) + } +}) + // ─── Asset registry (configurable assets, no contract redeploy) ───────────────── /** Public: list enabled assets for portfolio setup and frontend */ router.get('/assets', (_req: Request, res: Response) => { diff --git a/backend/src/db/migrations/008_legal_consent.down.sql b/backend/src/db/migrations/008_legal_consent.down.sql new file mode 100644 index 0000000..dd7e47d --- /dev/null +++ b/backend/src/db/migrations/008_legal_consent.down.sql @@ -0,0 +1,3 @@ +-- Migration: 008_legal_consent (down) +DROP INDEX IF EXISTS idx_legal_consent_updated; +DROP TABLE IF EXISTS legal_consent; diff --git a/backend/src/db/migrations/008_legal_consent.up.sql b/backend/src/db/migrations/008_legal_consent.up.sql new file mode 100644 index 0000000..67deacc --- /dev/null +++ b/backend/src/db/migrations/008_legal_consent.up.sql @@ -0,0 +1,18 @@ +-- Migration: 008_legal_consent (up) +-- Description: Store user consent for Terms of Service, Privacy Policy, and Cookie Policy (GDPR/CCPA). +-- Rollback: See 008_legal_consent.down.sql + +CREATE TABLE IF NOT EXISTS legal_consent ( + user_id VARCHAR(256) PRIMARY KEY, + terms_accepted_at TIMESTAMPTZ, + privacy_accepted_at TIMESTAMPTZ, + cookie_accepted_at TIMESTAMPTZ, + ip_address VARCHAR(64), + user_agent VARCHAR(512), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_legal_consent_updated ON legal_consent(updated_at); + +COMMENT ON TABLE legal_consent IS 'User acceptance of ToS, Privacy Policy, Cookie Policy for GDPR/CCPA compliance'; diff --git a/backend/src/services/databaseService.ts b/backend/src/services/databaseService.ts index 1eb618d..e27c593 100644 --- a/backend/src/services/databaseService.ts +++ b/backend/src/services/databaseService.ts @@ -121,6 +121,17 @@ CREATE TABLE IF NOT EXISTS assets ( updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_assets_enabled ON assets(enabled) WHERE enabled = 1; + +CREATE TABLE IF NOT EXISTS legal_consent ( + user_id TEXT PRIMARY KEY, + terms_accepted_at TEXT, + privacy_accepted_at TEXT, + cookie_accepted_at TEXT, + ip_address TEXT, + user_agent TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); ` // ───────────────────────────────────────────── @@ -585,6 +596,62 @@ export class DatabaseService { } } + // ────────────────────────────────────────── + // Legal consent (GDPR/CCPA) + // ────────────────────────────────────────── + + recordConsent( + userId: string, + opts: { terms: boolean; privacy: boolean; cookies: boolean; ipAddress?: string; userAgent?: string } + ): void { + const now = new Date().toISOString() + this.db.prepare( + `INSERT INTO legal_consent (user_id, terms_accepted_at, privacy_accepted_at, cookie_accepted_at, ip_address, user_agent, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(user_id) DO UPDATE SET + terms_accepted_at = COALESCE(excluded.terms_accepted_at, terms_accepted_at), + privacy_accepted_at = COALESCE(excluded.privacy_accepted_at, privacy_accepted_at), + cookie_accepted_at = COALESCE(excluded.cookie_accepted_at, cookie_accepted_at), + ip_address = excluded.ip_address, + user_agent = excluded.user_agent, + updated_at = excluded.updated_at` + ).run( + userId, + opts.terms ? now : null, + opts.privacy ? now : null, + opts.cookies ? now : null, + opts.ipAddress ?? null, + opts.userAgent ?? null, + now + ) + } + + getConsent(userId: string): { termsAcceptedAt: string | null; privacyAcceptedAt: string | null; cookieAcceptedAt: string | null } | undefined { + const row = this.db.prepare<[string], { terms_accepted_at: string | null; privacy_accepted_at: string | null; cookie_accepted_at: string | null }>( + 'SELECT terms_accepted_at, privacy_accepted_at, cookie_accepted_at FROM legal_consent WHERE user_id = ?' + ).get(userId) + if (!row) return undefined + return { + termsAcceptedAt: row.terms_accepted_at, + privacyAcceptedAt: row.privacy_accepted_at, + cookieAcceptedAt: row.cookie_accepted_at + } + } + + hasFullConsent(userId: string): boolean { + const c = this.getConsent(userId) + return Boolean(c?.termsAcceptedAt && c?.privacyAcceptedAt && c?.cookieAcceptedAt) + } + + deleteUserData(userId: string): void { + this.db.prepare('DELETE FROM legal_consent WHERE user_id = ?').run(userId) + const portfolios = this.db.prepare<[string], { id: string }>('SELECT id FROM portfolios WHERE user_address = ?').all(userId) + for (const p of portfolios) { + this.db.prepare('DELETE FROM rebalance_history WHERE portfolio_id = ?').run(p.id) + } + this.db.prepare('DELETE FROM portfolios WHERE user_address = ?').run(userId) + } + clearAll(): void { try { this.db.prepare('DELETE FROM rebalance_history').run() diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 638fc44..bdbe4bb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,13 +2,19 @@ import { useState, useEffect } from 'react' import Landing from './components/Landing' import Dashboard from './components/Dashboard' import PortfolioSetup from './components/PortfolioSetup' +import Legal from './components/Legal' +import ConsentGate from './components/ConsentGate' import { walletManager } from './utils/walletManager' import { WalletError } from './utils/walletAdapters' import { login as authLogin } from './services/authService' +import { api } from './config/api' +import type { LegalDocType } from './components/Legal' function App() { const [currentView, setCurrentView] = useState('landing') const [publicKey, setPublicKey] = useState(null) + const [pendingConsentPublicKey, setPendingConsentPublicKey] = useState(null) + const [legalDoc, setLegalDoc] = useState(null) const [isConnecting, setIsConnecting] = useState(false) const [error, setError] = useState(null) @@ -16,65 +22,81 @@ function App() { checkWalletConnection() }, []) + const checkConsent = async (userId: string): Promise => { + try { + const res = await api.get<{ accepted: boolean }>(`/api/consent/status?userId=${encodeURIComponent(userId)}`) + return !!res?.accepted + } catch { + return false + } + } + const checkWalletConnection = async () => { try { - const publicKey = await walletManager.reconnect() - if (publicKey) { - try { - await authLogin(publicKey) - } catch (_) { + const pk = await walletManager.reconnect() + if (pk) { + const accepted = await checkConsent(pk) + if (accepted) { + try { await authLogin(pk) } catch (_) {} + setPublicKey(pk) + setCurrentView('dashboard') + } else { + setPublicKey(pk) + setPendingConsentPublicKey(pk) } - setPublicKey(publicKey) - setCurrentView('dashboard') } - } catch (error) { - console.error('Error checking wallet connection:', error) + } catch (err) { + console.error('Error checking wallet connection:', err) } } const connectWallet = async () => { setIsConnecting(true) setError(null) - try { - const publicKey = walletManager.getPublicKey() - if (publicKey) { - try { - await authLogin(publicKey) - } catch (_) { - } - setPublicKey(publicKey) + const pk = walletManager.getPublicKey() + if (pk) { + try { await authLogin(pk) } catch (_) {} + setPublicKey(pk) setCurrentView('dashboard') } else { setError('No wallet connected. Please select a wallet first.') } - } catch (error: any) { - console.error('Wallet connection error:', error) - - if (error instanceof WalletError) { - if (error.code === 'USER_DECLINED') { - setError('Connection was declined. Please approve in your wallet.') - } else if (error.code === 'WALLET_NOT_INSTALLED') { - setError(`${error.walletType || 'Wallet'} is not installed. Please install it and refresh.`) - } else if (error.code === 'NETWORK_MISMATCH') { - setError('Network mismatch. Please check your wallet network settings.') - } else if (error.code === 'TIMEOUT') { - setError('Connection timed out. Please try again.') - } else { - setError(error.message || 'Failed to connect wallet.') - } - } else if (error.message === 'NO_WALLET_FOUND') { + } catch (err: any) { + console.error('Wallet connection error:', err) + if (err instanceof WalletError) { + if (err.code === 'USER_DECLINED') setError('Connection was declined. Please approve in your wallet.') + else if (err.code === 'WALLET_NOT_INSTALLED') setError(`${err.walletType || 'Wallet'} is not installed. Please install it and refresh.`) + else if (err.code === 'NETWORK_MISMATCH') setError('Network mismatch. Please check your wallet network settings.') + else if (err.code === 'TIMEOUT') setError('Connection timed out. Please try again.') + else setError(err.message || 'Failed to connect wallet.') + } else if (err.message === 'NO_WALLET_FOUND') { setError('No Stellar wallet detected. Please install Freighter, Rabet, or xBull wallet.') } else { - setError(error.message || 'Failed to connect wallet. Please try again.') + setError(err.message || 'Failed to connect wallet. Please try again.') } } finally { setIsConnecting(false) } } - const handleNavigate = (view: string) => { + const handleNeedsConsent = (pk: string) => { + setPendingConsentPublicKey(pk) + } + + const handleConsentAccepted = () => { + if (pendingConsentPublicKey) { + setPublicKey(pendingConsentPublicKey) + setPendingConsentPublicKey(null) + setCurrentView('dashboard') + } + } + + const handleNavigate = (view: string, legalDocType?: LegalDocType) => { setError(null) + if (legalDocType) setLegalDoc(legalDocType) + else if (view.startsWith('legal-')) setLegalDoc(view.replace('legal-', '') as LegalDocType) + else setLegalDoc(null) setCurrentView(view) } @@ -95,28 +117,40 @@ function App() { )} - {currentView === 'landing' && ( + {pendingConsentPublicKey ? ( + legalDoc ? ( + setLegalDoc(null)} /> + ) : ( + setLegalDoc(doc)} + /> + ) + ) : (currentView === 'legal-terms' || currentView === 'legal-privacy' || currentView === 'legal-cookies') && legalDoc ? ( + handleNavigate('landing')} + /> + ) : currentView === 'landing' ? ( - )} - - {currentView === 'dashboard' && ( + ) : currentView === 'dashboard' ? ( - )} - - {currentView === 'setup' && ( + ) : currentView === 'setup' ? ( - )} + ) : null} ) } diff --git a/frontend/src/components/ConsentGate.tsx b/frontend/src/components/ConsentGate.tsx new file mode 100644 index 0000000..164128a --- /dev/null +++ b/frontend/src/components/ConsentGate.tsx @@ -0,0 +1,28 @@ +/** + * ConsentGate.tsx — Full-page gate when user must accept ToS/Privacy/Cookies before using the app. + * Renders ConsentModal on a full-page background (reconnect or first-time flow). + */ + +import React from 'react' +import ConsentModal from './ConsentModal' +import type { LegalDocType } from './Legal' + +interface ConsentGateProps { + userId: string + onAccept: () => void + onOpenLegal: (doc: LegalDocType) => void +} + +const ConsentGate: React.FC = ({ userId, onAccept, onOpenLegal }) => { + return ( +
+ +
+ ) +} + +export default ConsentGate diff --git a/frontend/src/components/ConsentModal.tsx b/frontend/src/components/ConsentModal.tsx new file mode 100644 index 0000000..a6f2b22 --- /dev/null +++ b/frontend/src/components/ConsentModal.tsx @@ -0,0 +1,135 @@ +/** + * ConsentModal.tsx — ToS, Privacy, Cookie consent before using the app. + * Users must accept all before proceeding. Links open Legal pages. + */ + +import React, { useState } from 'react' +import { FileText, AlertCircle } from 'lucide-react' +import { api } from '../config/api' + +interface ConsentModalProps { + userId: string + onAccept: () => void + onOpenLegal: (doc: 'terms' | 'privacy' | 'cookies') => void +} + +const ConsentModal: React.FC = ({ userId, onAccept, onOpenLegal }) => { + const [terms, setTerms] = useState(false) + const [privacy, setPrivacy] = useState(false) + const [cookies, setCookies] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState(null) + + const allAccepted = terms && privacy && cookies + + const handleAccept = async () => { + if (!allAccepted) return + setSubmitting(true) + setError(null) + try { + await api.post('/api/consent', { + userId, + terms: true, + privacy: true, + cookies: true + }) + onAccept() + } catch (e) { + setError(e instanceof Error ? e.message : 'Failed to save consent. Please try again.') + } finally { + setSubmitting(false) + } + } + + return ( +
+
+
+ +

+ Accept to continue +

+
+

+ To use the Portfolio Rebalancer you must accept the following. You can read each document before accepting. +

+
+ + + +
+ {error && ( +
+ + {error} +
+ )} +
+ +
+
+
+ ) +} + +export default ConsentModal diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index f595095..877792b 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react' import { motion } from 'framer-motion' import { PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts' -import { TrendingUp, AlertCircle, RefreshCw, ArrowLeft, ExternalLink } from 'lucide-react' +import { TrendingUp, AlertCircle, RefreshCw, ArrowLeft, ExternalLink, Trash2 } from 'lucide-react' import ThemeToggle from './ThemeToggle' import { useTheme } from '../context/ThemeContext' import AssetCard from './AssetCard' @@ -170,6 +170,26 @@ const Dashboard: React.FC = ({ onNavigate, publicKey }) => { onNavigate('landing') } + /** GDPR: Delete all user data from the server, then logout and go to landing */ + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) + const [deleting, setDeleting] = useState(false) + const deleteMyData = async () => { + if (!publicKey) return + setDeleting(true) + try { + await api.delete(ENDPOINTS.USER_DATA_DELETE(publicKey)) + await authLogout(publicKey) + StellarWallet.disconnect() + setShowDeleteConfirm(false) + onNavigate('landing') + } catch (e) { + console.error('Delete data failed', e) + alert(e instanceof Error ? e.message : 'Failed to delete data. Please try again.') + } finally { + setDeleting(false) + } + } + // Create allocation data from portfolio data const allocationData = portfolioData?.allocations?.map((alloc: any, index: number) => ({ name: alloc.asset, @@ -352,6 +372,14 @@ const Dashboard: React.FC = ({ onNavigate, publicKey }) => { {publicKey ? ( <> + + + + + + )} +
{/* Tab Navigation */}
diff --git a/frontend/src/components/Landing.tsx b/frontend/src/components/Landing.tsx index dac08d1..5f5437b 100644 --- a/frontend/src/components/Landing.tsx +++ b/frontend/src/components/Landing.tsx @@ -3,15 +3,17 @@ import { motion } from 'framer-motion' import { TrendingUp, Shield, Zap, ArrowRight, X } from 'lucide-react' import ThemeToggle from './ThemeToggle' import { WalletSelector } from './WalletSelector' +import { api } from '../config/api' interface LandingProps { onNavigate: (view: string) => void onConnectWallet: () => Promise + onNeedsConsent?: (publicKey: string) => void isConnecting: boolean publicKey: string | null } -const Landing: React.FC = ({ onNavigate, onConnectWallet, isConnecting, publicKey }) => { +const Landing: React.FC = ({ onNavigate, onConnectWallet, onNeedsConsent, isConnecting, publicKey }) => { const [showWalletSelector, setShowWalletSelector] = useState(false) const [error, setError] = useState(null) @@ -20,9 +22,21 @@ const Landing: React.FC = ({ onNavigate, onConnectWallet, isConnec setError(null) } - const handleWalletSelected = async (_publicKey: string) => { + const handleWalletSelected = async (pk: string) => { setShowWalletSelector(false) - await onConnectWallet() + try { + const res = await api.get<{ accepted: boolean }>(`/api/consent/status?userId=${encodeURIComponent(pk)}`) + if (res?.accepted) { + await onConnectWallet() + } else if (onNeedsConsent) { + onNeedsConsent(pk) + } else { + await onConnectWallet() + } + } catch { + if (onNeedsConsent) onNeedsConsent(pk) + else await onConnectWallet() + } } const handleWalletError = (errorMsg: string) => { @@ -171,6 +185,33 @@ const Landing: React.FC = ({ onNavigate, onConnectWallet, isConnec
+ {/* Footer with legal links */} +
+
+ + + +
+
+ {showWalletSelector && (
void +} + +const LEGAL_DOCS: Record = { + terms: { + title: 'Terms of Service', + content: ( + <> +

Last updated: {new Date().toISOString().slice(0, 10)}

+

1. Acceptance

+

By connecting your wallet and using the Stellar Portfolio Rebalancer (“Service”), you agree to these Terms of Service. If you do not agree, do not use the Service.

+

2. Disclaimers

+

The Service is provided “as is” without warranties of any kind. We do not guarantee accuracy of prices, execution of rebalances, or availability. You use the Service at your own risk.

+

3. Smart contract and protocol risks

+

Rebalancing may involve smart contracts and on-chain transactions. You acknowledge risks including but not limited to: contract bugs, oracle inaccuracies, slippage, network congestion, and irreversible transactions. We are not liable for any losses arising from use of the Service or underlying protocols.

+

4. Financial and tax

+

The Service does not constitute financial, investment, or tax advice. You are solely responsible for your investment decisions and any tax obligations.

+

5. Limitation of liability

+

To the maximum extent permitted by law, we shall not be liable for any indirect, incidental, special, or consequential damages arising from your use of the Service.

+

6. Changes

+

We may update these Terms. Continued use after changes constitutes acceptance. We will indicate the last updated date at the top.

+ + ) + }, + privacy: { + title: 'Privacy Policy', + content: ( + <> +

Last updated: {new Date().toISOString().slice(0, 10)}. This policy is GDPR and CCPA compliant where applicable.

+

1. Data we collect

+

We collect: (a) wallet public address when you connect; (b) portfolio configuration and rebalance history you create; (c) consent timestamps and IP/user agent for legal compliance; (d) notification preferences if you opt in.

+

2. Purpose and legal basis

+

We process data to provide the Service, comply with legal obligations (e.g. consent records), and improve the product. Legal bases: contract performance, consent, and legitimate interest.

+

3. Your rights (GDPR / CCPA)

+

You have the right to: access your data, export your data (via the in-app export feature), rectify inaccuracies, request deletion of your data, object to processing, and withdraw consent. To exercise these rights, use the “Export my data” and “Delete my data” options in the dashboard or contact us.

+

4. Data retention

+

We retain your data until you request deletion or we no longer need it for the purposes stated. Consent records may be retained as required for legal compliance.

+

5. Security and sharing

+

We implement appropriate technical and organizational measures to protect your data. We do not sell your personal data. We may share data with service providers (e.g. hosting) under strict agreements.

+ + ) + }, + cookies: { + title: 'Cookie Policy', + content: ( + <> +

Last updated: {new Date().toISOString().slice(0, 10)}

+

1. What we use

+

We use strictly necessary cookies and local storage to: keep you logged in (e.g. JWT), remember your consent choices, and store preferences. We do not use third-party advertising cookies.

+

2. Your control

+

You can withdraw cookie consent via the consent banner or by clearing site data. Note that rejecting necessary cookies may prevent the Service from functioning.

+

3. Updates

+

We may update this Cookie Policy. We will indicate the last updated date at the top.

+ + ) + } +} + +const Legal: React.FC = ({ doc, onBack }) => { + const { title, content } = LEGAL_DOCS[doc] + return ( +
+
+ +
+ +

{title}

+
+
+ {content} +
+
+ +
+
+
+ ) +} + +export default Legal +export { LEGAL_DOCS } diff --git a/frontend/src/config/api.ts b/frontend/src/config/api.ts index 93dfc65..0fb52a8 100644 --- a/frontend/src/config/api.ts +++ b/frontend/src/config/api.ts @@ -90,6 +90,9 @@ export const API_CONFIG = { RISK_CHECK: (portfolioId: string) => `/api/risk/check/${portfolioId}`, TEST_CORS: '/test/cors', TEST_COINGECKO: '/test/coingecko', + CONSENT_STATUS: '/api/consent/status', + CONSENT_RECORD: '/api/consent', + USER_DATA_DELETE: (address: string) => `/api/user/${address}/data`, } }