fix(admin): admin/profile routes and services
This commit is contained in:
parent
79b51b1c9a
commit
426b0fd932
|
|
@ -412,10 +412,6 @@ export const api = {
|
|||
bankSyncConfig: () => get('/admin/bank-sync-config'),
|
||||
setBankSyncConfig: (data) => put('/admin/bank-sync-config', data),
|
||||
|
||||
// Admin — privacy settings
|
||||
privacySettings: () => get('/admin/privacy'),
|
||||
setPrivacySettings: (data) => put('/admin/privacy', data),
|
||||
|
||||
// User SQLite import
|
||||
previewUserDbImport: async (file) => {
|
||||
const csrfToken = await getCsrfToken();
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import UsersTable from '@/components/admin/UsersTable';
|
|||
import AddUserCard from '@/components/admin/AddUserCard';
|
||||
import BackupManagementCard from '@/components/admin/BackupManagementCard';
|
||||
import CleanupPanel from '@/components/admin/CleanupPanel';
|
||||
import PrivacyAdminCard from '@/components/admin/PrivacyAdminCard';
|
||||
|
||||
export default function AdminPage() {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -85,7 +84,6 @@ export default function AdminPage() {
|
|||
<main className="mx-auto max-w-5xl px-4 py-6 sm:px-6 lg:px-8 lg:py-8 space-y-6">
|
||||
<EmailNotifCard />
|
||||
<BankSyncAdminCard />
|
||||
<PrivacyAdminCard />
|
||||
<BackupManagementCard />
|
||||
<CleanupPanel />
|
||||
<LoginModeCard users={users} onModeChange={setAuthMode} />
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback } from 'react';
|
|||
import { toast } from 'sonner';
|
||||
import {
|
||||
User, Mail, KeyRound, ShieldCheck, Loader2, History, Monitor, Smartphone, ChevronRight,
|
||||
Bell, SendHorizontal, ScanLine, TriangleAlert, Copy, Check,
|
||||
Bell, SendHorizontal, ScanLine, TriangleAlert, Copy, Check, Lock,
|
||||
} from 'lucide-react';
|
||||
import { api } from '@/api';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
|
|
@ -848,11 +848,58 @@ function TotpSection() {
|
|||
);
|
||||
}
|
||||
|
||||
function PrivacySettings({ settings, onSaved }) {
|
||||
const [geoEnabled, setGeoEnabled] = useState(!!settings.geolocation_enabled);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => { setGeoEnabled(!!settings.geolocation_enabled); }, [settings.geolocation_enabled]);
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await api.updateProfileSettings({ geolocation_enabled: geoEnabled });
|
||||
toast.success('Privacy settings saved.');
|
||||
onSaved({ ...settings, geolocation_enabled: geoEnabled });
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to save privacy settings.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const changed = geoEnabled !== !!settings.geolocation_enabled;
|
||||
|
||||
return (
|
||||
<SectionCard title="Privacy" icon={Lock} subtitle="Control what login data is collected about you.">
|
||||
<div className="px-6 py-5 space-y-4">
|
||||
<CheckRow
|
||||
id="geo-enabled"
|
||||
label="Login geolocation"
|
||||
checked={geoEnabled}
|
||||
onChange={setGeoEnabled}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When on, your login IP is resolved to a city/region via{' '}
|
||||
<span className="font-mono text-[11px]">ip-api.com</span> over plain HTTP.
|
||||
Location data is encrypted at rest and visible only to you. Turn off to keep
|
||||
all login data on-device.
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-6 py-4 border-t border-border/50 flex justify-end">
|
||||
<Button onClick={save} disabled={saving || !changed}>
|
||||
{saving ? <><Loader2 className="h-4 w-4 mr-2 animate-spin" />Saving…</> : 'Save Privacy Settings'}
|
||||
</Button>
|
||||
</div>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileNav() {
|
||||
const items = [
|
||||
['#account', 'Account'],
|
||||
['#security', 'Security'],
|
||||
['#notifications', 'Notifications'],
|
||||
['#privacy', 'Privacy'],
|
||||
];
|
||||
return (
|
||||
<div className="mb-6 flex flex-wrap gap-2">
|
||||
|
|
@ -925,6 +972,9 @@ export default function ProfilePage() {
|
|||
{!loading && <NotificationPreferences settings={settings} onSaved={setSettings} />}
|
||||
{!loading && <PushNotifications settings={settings} onSaved={() => api.profileSettings().then(setSettings).catch(() => {})} />}
|
||||
</div>
|
||||
<div id="privacy" className="scroll-mt-6">
|
||||
{!loading && <PrivacySettings settings={settings} onSaved={setSettings} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3359,6 +3359,17 @@ function runMigrations() {
|
|||
console.log('[v1.01] transactions.pending flag + partial index added');
|
||||
}
|
||||
},
|
||||
{
|
||||
version: 'v1.02',
|
||||
description: 'users: per-user geolocation opt-in (was global admin setting)',
|
||||
run() {
|
||||
const cols = db.prepare('PRAGMA table_info(users)').all().map(c => c.name);
|
||||
if (!cols.includes('geolocation_enabled')) {
|
||||
db.exec('ALTER TABLE users ADD COLUMN geolocation_enabled INTEGER NOT NULL DEFAULT 0');
|
||||
}
|
||||
console.log('[v1.02] users.geolocation_enabled added');
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// ── users: notification columns ───────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb, getSetting, setSetting, rollbackMigration } = require('../db/database');
|
||||
const { getDb, rollbackMigration } = require('../db/database');
|
||||
const { getBankSyncConfig, setBankSyncEnabled, setSyncIntervalHours, setSyncDays, setDebugLogging } = require('../services/bankSyncConfigService');
|
||||
const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker');
|
||||
const { isEnvKeyActive } = require('../services/encryptionService');
|
||||
|
|
@ -458,26 +458,6 @@ router.put('/bank-sync-config', (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// ── Privacy Settings ──────────────────────────────────────────────────────────
|
||||
|
||||
// GET /api/admin/privacy
|
||||
router.get('/privacy', (req, res) => {
|
||||
res.json({
|
||||
geolocation_enabled: getSetting('geolocation_enabled') === 'true',
|
||||
});
|
||||
});
|
||||
|
||||
// PUT /api/admin/privacy
|
||||
router.put('/privacy', (req, res) => {
|
||||
const { geolocation_enabled } = req.body || {};
|
||||
if (typeof geolocation_enabled === 'boolean') {
|
||||
setSetting('geolocation_enabled', geolocation_enabled ? 'true' : 'false');
|
||||
}
|
||||
res.json({
|
||||
geolocation_enabled: getSetting('geolocation_enabled') === 'true',
|
||||
});
|
||||
});
|
||||
|
||||
// ── Migration Rollback ────────────────────────────────────────────────────────
|
||||
router.post('/migrations/rollback', async (req, res) => {
|
||||
const { version } = req.body;
|
||||
|
|
|
|||
|
|
@ -147,7 +147,8 @@ router.get('/settings', (req, res) => {
|
|||
const user = db.prepare(`
|
||||
SELECT notification_email, notifications_enabled,
|
||||
notify_3d, notify_1d, notify_due, notify_overdue, notify_amount_change,
|
||||
notify_push_enabled, push_channel, push_url, push_token, push_chat_id
|
||||
notify_push_enabled, push_channel, push_url, push_token, push_chat_id,
|
||||
geolocation_enabled
|
||||
FROM users WHERE id = ?
|
||||
`).get(req.user.id);
|
||||
|
||||
|
|
@ -169,6 +170,7 @@ router.get('/settings', (req, res) => {
|
|||
push_url: pushUrlDecrypted || null,
|
||||
push_token_set: !!user.push_token,
|
||||
push_chat_id: user.push_chat_id || null,
|
||||
geolocation_enabled: !!user.geolocation_enabled,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -183,6 +185,7 @@ router.patch('/settings', (req, res) => {
|
|||
notification_email, email, notifications_enabled,
|
||||
notify_3d, notify_1d, notify_due, notify_overdue, notify_amount_change,
|
||||
notify_push_enabled, push_channel, push_url, push_token, push_chat_id,
|
||||
geolocation_enabled,
|
||||
} = req.body;
|
||||
|
||||
const nextEmail = notification_email !== undefined ? notification_email : email;
|
||||
|
|
@ -203,7 +206,8 @@ router.patch('/settings', (req, res) => {
|
|||
const current = db.prepare(`
|
||||
SELECT notification_email, notifications_enabled,
|
||||
notify_3d, notify_1d, notify_due, notify_overdue, notify_amount_change,
|
||||
notify_push_enabled, push_channel, push_url, push_token, push_chat_id
|
||||
notify_push_enabled, push_channel, push_url, push_token, push_chat_id,
|
||||
geolocation_enabled
|
||||
FROM users WHERE id = ?
|
||||
`).get(req.user.id);
|
||||
|
||||
|
|
@ -235,6 +239,7 @@ router.patch('/settings', (req, res) => {
|
|||
push_url = ?,
|
||||
push_token = ?,
|
||||
push_chat_id = ?,
|
||||
geolocation_enabled = ?,
|
||||
updated_at = datetime('now')
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
|
|
@ -250,6 +255,7 @@ router.patch('/settings', (req, res) => {
|
|||
encryptOrNull(push_url, current.push_url),
|
||||
encryptOrNull(push_token, current.push_token),
|
||||
push_chat_id !== undefined ? (push_chat_id?.trim() || null) : current.push_chat_id,
|
||||
boolVal(geolocation_enabled, current.geolocation_enabled),
|
||||
req.user.id,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const crypto = require('crypto');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { getDb, getSetting } = require('../db/database');
|
||||
const { getDb } = require('../db/database');
|
||||
const { buildDeviceFingerprint } = require('./loginFingerprint');
|
||||
const { encryptSecret, decryptSecret } = require('./encryptionService');
|
||||
|
||||
|
|
@ -262,8 +262,9 @@ function recordLogin(userId, ipAddress, userAgent, sessionId) {
|
|||
// Background tasks: geolocation + new device push alert
|
||||
if (insertedId) {
|
||||
setImmediate(() => {
|
||||
// Geolocation — only when admin has opted in, and skip private/loopback IPs
|
||||
if (ipAddress && getSetting('geolocation_enabled') === 'true') {
|
||||
// Geolocation — only when the user has opted in, and skip private/loopback IPs
|
||||
const loginUser = getDb().prepare('SELECT geolocation_enabled FROM users WHERE id = ?').get(userId);
|
||||
if (ipAddress && loginUser?.geolocation_enabled) {
|
||||
const isPrivate = /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|127\.|::1|localhost)/i.test(ipAddress);
|
||||
if (!isPrivate) {
|
||||
fetch(`http://ip-api.com/json/${ipAddress}?fields=status,city,country,regionName,isp`, { signal: AbortSignal.timeout(5000) })
|
||||
|
|
|
|||
Loading…
Reference in New Issue