123 lines
4.8 KiB
TypeScript
123 lines
4.8 KiB
TypeScript
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
|
|
import type { Req, Res, Next } from '../types/http';
|
|
('use strict');
|
|
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const { getDb } = require('../db/database.cts');
|
|
const { seedDemoData } = require('../scripts/seedDemoData.cts');
|
|
const { demoDataLimiter } = require('../middleware/rateLimiter.cts');
|
|
const { eraseUserData, clearSeededDemoData } = require('../services/userDataService.cts');
|
|
const { ApiError, ValidationError } = require('../utils/apiError.cts');
|
|
|
|
// 4xx service errors keep their message/code on the wire; anything else is
|
|
// rethrown raw so the terminal handler masks + logs it as a 5xx.
|
|
function toUserOpError(err, fallbackCode) {
|
|
if (err.status && err.status < 500) {
|
|
return new ApiError(err.code || fallbackCode, err.message, err.status);
|
|
}
|
|
return err;
|
|
}
|
|
|
|
// GET /api/user/seeded-status — whether the current user has demo data, with counts
|
|
router.get('/seeded-status', (req: Req, res: Res) => {
|
|
try {
|
|
const db = getDb();
|
|
const userId = req.user.id;
|
|
const one = (sql: string) => db.prepare(sql).get(userId).count as number;
|
|
|
|
const seededBills = one(
|
|
'SELECT COUNT(*) as count FROM bills WHERE user_id = ? AND is_seeded = 1',
|
|
);
|
|
const seededCategories = one(
|
|
'SELECT COUNT(*) as count FROM categories WHERE user_id = ? AND is_seeded = 1',
|
|
);
|
|
// Bill-children (cascade with the seeded bill) counted via their bill.
|
|
const seededPayments = one(
|
|
'SELECT COUNT(*) as count FROM payments WHERE bill_id IN (SELECT id FROM bills WHERE user_id = ? AND is_seeded = 1)',
|
|
);
|
|
const seededTransactions = one(
|
|
'SELECT COUNT(*) as count FROM transactions WHERE user_id = ? AND is_seeded = 1',
|
|
);
|
|
|
|
res.json({
|
|
seeded: seededBills > 0 || seededCategories > 0,
|
|
seededBills,
|
|
seededCategories,
|
|
seededPayments,
|
|
seededTransactions,
|
|
});
|
|
} catch (err) {
|
|
throw toUserOpError(err, 'SEEDED_STATUS_ERROR');
|
|
}
|
|
});
|
|
|
|
// POST /api/user/clear-demo-data — surgically remove ONLY seeded demo data for the
|
|
// requesting user (in one transaction, child → parent). Marker tables are removed
|
|
// by is_seeded; bill-children cascade with their seeded bill. A seeded category is
|
|
// removed only if no remaining (non-seeded) bill references it, so a user's own bill
|
|
// that reused a demo category keeps its category. Mirrors eraseUserData().
|
|
router.post('/clear-demo-data', demoDataLimiter, (req: Req, res: Res) => {
|
|
try {
|
|
const result = clearSeededDemoData(getDb(), req.user.id);
|
|
res.json({
|
|
success: true,
|
|
removed: result.removed,
|
|
billsDeleted: result.counts.bills || 0,
|
|
categoriesDeleted: result.counts.categories || 0,
|
|
counts: result.counts,
|
|
});
|
|
} catch (err) {
|
|
throw toUserOpError(err, 'CLEAR_DEMO_ERROR');
|
|
}
|
|
});
|
|
|
|
// POST /api/user/seed-demo-data — seed the full demo dataset for the requesting user
|
|
router.post('/seed-demo-data', demoDataLimiter, (req: Req, res: Res) => {
|
|
try {
|
|
const db = getDb();
|
|
const result = seedDemoData(req.user.id);
|
|
db.prepare(
|
|
`INSERT INTO import_history (user_id, imported_at, source_filename, file_type, rows_parsed, rows_created, rows_updated, rows_skipped, rows_ambiguous, rows_errored, options_json, summary_json)
|
|
VALUES (?, datetime('now'), 'seed-demo-data', 'seed-demo', ?, ?, 0, 0, 0, 0, ?, ?)`,
|
|
).run(
|
|
req.user.id,
|
|
result.billsCreated + result.paymentsCreated + result.transactionsCreated,
|
|
result.billsCreated,
|
|
JSON.stringify({ action: 'seed-demo-data' }),
|
|
JSON.stringify(result.counts),
|
|
);
|
|
res.json({
|
|
success: true,
|
|
message: `Created ${result.billsCreated} bills, ${result.paymentsCreated} payments and ${result.transactionsCreated} transactions`,
|
|
billsCreated: result.billsCreated,
|
|
categoriesCreated: result.categoriesCreated,
|
|
paymentsCreated: result.paymentsCreated,
|
|
transactionsCreated: result.transactionsCreated,
|
|
counts: result.counts,
|
|
});
|
|
} catch (err) {
|
|
throw toUserOpError(err, 'SEED_ERROR');
|
|
}
|
|
});
|
|
|
|
// POST /api/user/erase-data — permanently wipe the requesting user's financial +
|
|
// imported data (bills, payments, transactions, connections, categories, imports).
|
|
// Preserves the account, login, 2FA, and preferences. Requires a type-to-confirm token.
|
|
router.post('/erase-data', demoDataLimiter, (req: Req, res: Res) => {
|
|
const confirm = String(req.body?.confirm ?? '')
|
|
.trim()
|
|
.toUpperCase();
|
|
if (confirm !== 'ERASE') {
|
|
throw ValidationError('Type ERASE to confirm.', 'confirm', 'ERASE_CONFIRM_REQUIRED');
|
|
}
|
|
try {
|
|
const result = eraseUserData(getDb(), req.user.id);
|
|
res.json({ success: true, ...result });
|
|
} catch (err) {
|
|
throw toUserOpError(err, 'ERASE_ERROR');
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|