Skip to content

Commit aeb0e12

Browse files
authored
Merge pull request #414 from Markodiba/feature/compliance-dashboard-and-backup-ui
Add compliance dashboard and backup UI
2 parents ca9a41f + d6d8d86 commit aeb0e12

7 files changed

Lines changed: 608 additions & 2 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { verifyToken } from '../auth/tokens.js';
2+
3+
// Admin role check middleware
4+
// For demo purposes, we check if user has an 'admin' role in their JWT payload
5+
// In production, this should check against a database or external auth service
6+
export function requireAdmin(req, res, next) {
7+
const auth = req.headers.authorization;
8+
if (!auth?.startsWith('Bearer ')) {
9+
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
10+
}
11+
12+
try {
13+
const user = verifyToken(auth.slice(7));
14+
req.user = user;
15+
16+
// Check if user has admin role
17+
if (user.role !== 'admin') {
18+
return res.status(403).json({ error: 'Admin access required' });
19+
}
20+
21+
next();
22+
} catch {
23+
res.status(401).json({ error: 'Invalid or expired token' });
24+
}
25+
}

backend/src/routes/backup.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,28 @@ router.get('/metrics', (_req, res) => {
6565
res.json(getMetrics());
6666
});
6767

68+
// GET /api/backup/status — get last backup info for UI
69+
router.get('/status', async (_req, res) => {
70+
try {
71+
const backups = await listBackups();
72+
const lastBackup = backups.length > 0 ? backups[0] : null;
73+
const metrics = getMetrics();
74+
75+
res.json({
76+
lastBackup: lastBackup ? {
77+
timestamp: lastBackup.createdAt,
78+
file: lastBackup.file,
79+
size: lastBackup.size,
80+
} : null,
81+
metrics: {
82+
totalBackups: backups.length,
83+
totalSize: backups.reduce((sum, b) => sum + b.size, 0),
84+
...metrics,
85+
},
86+
});
87+
} catch (err) {
88+
res.status(500).json({ error: err.message });
89+
}
90+
});
91+
6892
export default router;

backend/src/routes/compliance.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Router } from 'express';
22
import { requireAuth as authMiddleware } from '../middleware/auth.js';
3+
import { requireAdmin } from '../middleware/adminAuth.js';
4+
import { prisma } from '../db/client.js';
35
import {
46
kycCollector,
57
identityVerifier,
@@ -88,4 +90,74 @@ router.get('/reports', authMiddleware, async (req, res) => {
8890
res.json(reports);
8991
});
9092

93+
// ── AML Alerts Dashboard (Admin Only) ────────────────────────────────────────
94+
95+
// List all AML alerts with pagination
96+
router.get('/aml/alerts', requireAdmin, async (req, res) => {
97+
try {
98+
const { page = 1, limit = 20, severity, reviewed } = req.query;
99+
const skip = (parseInt(page) - 1) * parseInt(limit);
100+
101+
const where = {};
102+
if (severity) where.severity = severity;
103+
104+
const alerts = await prisma.aMLAlert.findMany({
105+
where,
106+
include: {
107+
transaction: {
108+
select: {
109+
hash: true,
110+
amount: true,
111+
assetCode: true,
112+
createdAt: true,
113+
},
114+
},
115+
user: {
116+
select: {
117+
publicKey: true,
118+
},
119+
},
120+
},
121+
orderBy: { createdAt: 'desc' },
122+
skip,
123+
take: parseInt(limit),
124+
});
125+
126+
const total = await prisma.aMLAlert.count({ where });
127+
128+
res.json({
129+
alerts,
130+
pagination: {
131+
page: parseInt(page),
132+
limit: parseInt(limit),
133+
total,
134+
pages: Math.ceil(total / parseInt(limit)),
135+
},
136+
});
137+
} catch (err) {
138+
res.status(500).json({ error: err.message });
139+
}
140+
});
141+
142+
// Mark alert as reviewed (admin only)
143+
router.patch('/aml/alerts/:id/review', requireAdmin, async (req, res) => {
144+
try {
145+
const { id } = req.params;
146+
const { notes } = req.body;
147+
148+
// For now, we'll log the review action in the audit trail
149+
// In a production system, you'd add a 'reviewed' field to the AMLAlert model
150+
await complianceAudit.log('AML_ALERT_REVIEWED', req.user.id, {
151+
alertId: id,
152+
reviewedBy: req.user.id,
153+
notes: notes || '',
154+
reviewedAt: new Date().toISOString(),
155+
});
156+
157+
res.json({ success: true, message: 'Alert marked as reviewed' });
158+
} catch (err) {
159+
res.status(500).json({ error: err.message });
160+
}
161+
});
162+
91163
export default router;

frontend/src/App.jsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import { useExchangeRate } from './hooks/useExchangeRate';
4444
import { AMMPoolBrowser } from './components/AMMPoolBrowser';
4545
import { ConvertWidget } from './components/ConvertWidget';
4646
import { AccountRecovery } from './components/AccountRecovery';
47+
import { ComplianceDashboard } from './components/ComplianceDashboard';
48+
import { BackupSettings } from './components/BackupSettings';
4749

4850
const STATUS_COLORS = { connected: '#22c55e', disconnected: '#ef4444', reconnecting: '#f59e0b' };
4951
const TIMEOUT_MS = 30000;
@@ -71,6 +73,9 @@ function App() {
7173
const [kycStatus, setKycStatus] = useState(null);
7274
const [kycLoading, setKycLoading] = useState(false);
7375
const [kycError, setKycError] = useState(null);
76+
const [showComplianceDashboard, setShowComplianceDashboard] = useState(false);
77+
const [showBackupSettings, setShowBackupSettings] = useState(false);
78+
const [userRole, setUserRole] = useState(null);
7479

7580
const msg = useMessages();
7681
const { canInstall, install, updateAvailable, applyUpdate, pushEnabled, enablePush } = usePWA();
@@ -214,6 +219,16 @@ function App() {
214219

215220
useEffect(() => {
216221
fetchKycStatus();
222+
// Check if user has admin role from JWT token
223+
const token = localStorage.getItem('authToken');
224+
if (token) {
225+
try {
226+
const payload = JSON.parse(atob(token.split('.')[1]));
227+
setUserRole(payload.role);
228+
} catch (e) {
229+
// Invalid token format
230+
}
231+
}
217232
}, [fetchKycStatus]);
218233

219234
const saveLabel = async () => {
@@ -902,11 +917,19 @@ function App() {
902917
{ id: 'multisig', label: '🔐 Multi-Sig' },
903918
{ id: 'kyc', label: '📋 KYC' },
904919
{ id: 'notifications', label: '🔔 Notifications' },
920+
{ id: 'backup', label: '💾 Backup', action: () => setShowBackupSettings(true) },
921+
...(userRole === 'admin' ? [{ id: 'compliance', label: '🛡️ Compliance', action: () => setShowComplianceDashboard(true) }] : []),
905922
].map((section) => (
906923
<button
907924
key={section.id}
908925
type="button"
909-
onClick={() => setActiveSettingsSection(activeSettingsSection === section.id ? null : section.id)}
926+
onClick={() => {
927+
if (section.action) {
928+
section.action();
929+
} else {
930+
setActiveSettingsSection(activeSettingsSection === section.id ? null : section.id);
931+
}
932+
}}
910933
style={{
911934
padding: '10px 16px',
912935
background: activeSettingsSection === section.id ? '#2563eb' : '#f3f4f6',
@@ -1036,6 +1059,14 @@ function App() {
10361059
onClose={() => setShowSettings(false)}
10371060
/>
10381061
)}
1062+
1063+
{showComplianceDashboard && (
1064+
<ComplianceDashboard onClose={() => setShowComplianceDashboard(false)} />
1065+
)}
1066+
1067+
{showBackupSettings && (
1068+
<BackupSettings onClose={() => setShowBackupSettings(false)} />
1069+
)}
10391070
</div>
10401071
</>
10411072
);

frontend/src/components/AccountSettings.jsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
22
import axios from 'axios';
33
import { AddressBook } from './AddressBook';
44
import { WebhookManager } from './WebhookManager';
5+
import { BackupSettings } from './BackupSettings';
6+
import { ComplianceDashboard } from './ComplianceDashboard';
57

68
const ASSETS = ['XLM', 'USDC', 'EURC'];
79

@@ -10,10 +12,25 @@ export function AccountSettings({ publicKey, onClose }) {
1012
const [saving, setSaving] = useState(false);
1113
const [error, setError] = useState(null);
1214
const [saved, setSaved] = useState(false);
15+
const [showBackup, setShowBackup] = useState(false);
16+
const [showCompliance, setShowCompliance] = useState(false);
17+
const [userRole, setUserRole] = useState(null);
1318

1419
useEffect(() => {
1520
axios.get(`/api/stellar/account/${publicKey}/settings`)
16-
.then(({ data }) => setSettings(data))
21+
.then(({ data }) => {
22+
setSettings(data);
23+
// Check if user has admin role from JWT token
24+
const token = localStorage.getItem('authToken');
25+
if (token) {
26+
try {
27+
const payload = JSON.parse(atob(token.split('.')[1]));
28+
setUserRole(payload.role);
29+
} catch (e) {
30+
// Invalid token format
31+
}
32+
}
33+
})
1734
.catch(e => setError(e?.response?.data?.error ?? e.message));
1835
}, [publicKey]);
1936

@@ -110,6 +127,25 @@ export function AccountSettings({ publicKey, onClose }) {
110127
<WebhookManager accountId={publicKey} />
111128
</div>
112129

130+
<div style={{ marginBottom: 16, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
131+
<button
132+
type="button"
133+
onClick={() => setShowBackup(true)}
134+
style={{ fontSize: '0.9rem', padding: '8px 16px' }}
135+
>
136+
💾 Backup & Restore
137+
</button>
138+
{userRole === 'admin' && (
139+
<button
140+
type="button"
141+
onClick={() => setShowCompliance(true)}
142+
style={{ fontSize: '0.9rem', padding: '8px 16px', background: '#dc2626' }}
143+
>
144+
🛡️ Compliance Dashboard
145+
</button>
146+
)}
147+
</div>
148+
113149
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
114150
<button type="button" onClick={save} disabled={saving}>
115151
{saving ? 'Saving…' : 'Save'}
@@ -119,6 +155,9 @@ export function AccountSettings({ publicKey, onClose }) {
119155
</div>
120156
</>
121157
)}
158+
159+
{showBackup && <BackupSettings onClose={() => setShowBackup(false)} />}
160+
{showCompliance && <ComplianceDashboard onClose={() => setShowCompliance(false)} />}
122161
</div>
123162
</div>
124163
);

0 commit comments

Comments
 (0)