all.ts 21.6 KB
Newer Older
Benjamin Bouvier's avatar
Benjamin Bouvier committed
1
import express from 'express';
2
import crypto from 'crypto';
3

Benjamin Bouvier's avatar
Benjamin Bouvier committed
4
import { Access, Account, Alert, Budget, Category, Setting, Transaction } from '../models';
5

Nicolas Frandeboeuf's avatar
Nicolas Frandeboeuf committed
6
7
import runDataMigrations from '../models/data-migrations';

8
import {
9
    assert,
10
    makeLogger,
11
    isEmailEnabled,
12
13
14
    KError,
    asyncErr,
    getErrorCode,
15
    UNKNOWN_OPERATION_TYPE,
16
    isAppriseApiEnabled,
17
18
} from '../helpers';

19
import { bankVendorByUuid } from '../lib/bank-vendors';
20
21
22
23
import { ConfigGhostSettings } from '../lib/ghost-settings';
import { validatePassword } from '../shared/helpers';
import DefaultSettings from '../shared/default-settings';

Benjamin Bouvier's avatar
Benjamin Bouvier committed
24
import { cleanData, Remapping } from './helpers';
25
26
import { isDemoEnabled } from './settings';
import { ofxToKresus } from './ofx';
Benjamin Bouvier's avatar
Benjamin Bouvier committed
27
import { IdentifiedRequest } from './routes';
28

29
const log = makeLogger('controllers/all');
30
31
32

const ERR_MSG_LOADING_ALL = 'Error when loading all Kresus data';

Benjamin Bouvier's avatar
Benjamin Bouvier committed
33
34
35
36
37
38
39
interface GetAllDataOptions {
    isExport?: boolean;
    cleanPassword?: boolean;
}

// FIXME also contains all the fields from Access.
interface ClientAccess {
40
    vendorId: string;
Benjamin Bouvier's avatar
Benjamin Bouvier committed
41
42
43
44
    enabled?: boolean;
    fields: { name: string; value: string }[];
    password?: string | null;
    session?: string | null;
45
46
    customLabel: string | null;
    label?: string | null;
Benjamin Bouvier's avatar
Benjamin Bouvier committed
47
48
49
50
51
52
53
54
55
56
57
58
}

interface AllData {
    accounts: Account[];
    accesses: ClientAccess[];
    alerts: Alert[];
    categories: Category[];
    operations: Transaction[];
    settings: Setting[];
    budgets?: Budget[];
}

59
async function getAllData(userId: number, options: GetAllDataOptions = {}): Promise<AllData> {
60
    const { isExport = false, cleanPassword = true } = options;
Antoine's avatar
Antoine committed
61

Benjamin Bouvier's avatar
Benjamin Bouvier committed
62
63
64
65
66
67
68
69
    const ret: AllData = {
        accounts: [],
        accesses: [],
        alerts: [],
        categories: [],
        operations: [],
        settings: [],
    };
70

Benjamin Bouvier's avatar
Benjamin Bouvier committed
71
    const accesses = await Access.all(userId);
72
    for (const access of accesses) {
Benjamin Bouvier's avatar
Benjamin Bouvier committed
73
        const clientAccess: ClientAccess = { ...access };
74

75
        // Just keep the name and the value of the field.
Benjamin Bouvier's avatar
Benjamin Bouvier committed
76
        clientAccess.fields = (access.fields || []).map(({ name, value }) => {
77
78
79
            return { name, value };
        });

80
        if (cleanPassword) {
Benjamin Bouvier's avatar
Benjamin Bouvier committed
81
82
            delete clientAccess.password;
            delete clientAccess.session;
83
        }
84
85

        if (!isExport) {
86
            // Process enabled status only for the /all request.
Benjamin Bouvier's avatar
Benjamin Bouvier committed
87
88
            clientAccess.enabled = access.isEnabled();
            delete clientAccess.session;
89
        }
90

91
92
93
94
95
        const bank = bankVendorByUuid(clientAccess.vendorId);
        if (bank && bank.name) {
            clientAccess.label = bank.name;
        }

Benjamin Bouvier's avatar
Benjamin Bouvier committed
96
        ret.accesses.push(clientAccess);
Antoine's avatar
Antoine committed
97
98
    }

Benjamin Bouvier's avatar
Benjamin Bouvier committed
99
    ret.accounts = await Account.all(userId);
100
101
102
    ret.categories = await Category.all(userId);
    ret.operations = await Transaction.all(userId);
    ret.settings = isExport ? await Setting.allWithoutGhost(userId) : await Setting.all(userId);
103

104
    if (isExport) {
105
        ret.budgets = await Budget.all(userId);
106
107
    }

108
    if (isExport || isEmailEnabled() || isAppriseApiEnabled()) {
109
        ret.alerts = await Alert.all(userId);
110
111
112
113
    } else {
        ret.alerts = [];
    }

114
    return ret;
115
116
}

Benjamin Bouvier's avatar
Benjamin Bouvier committed
117
export async function all(req: IdentifiedRequest<any>, res: express.Response) {
118
    try {
119
120
        const { id: userId } = req.user;
        const ret = await getAllData(userId);
121
        res.status(200).json(ret);
122
    } catch (err) {
123
        err.code = ERR_MSG_LOADING_ALL;
124
        asyncErr(res, err, 'when loading all data');
125
    }
126
127
}

128
129
130
const ENCRYPTION_ALGORITHM = 'aes-256-ctr';
const ENCRYPTED_CONTENT_TAG = Buffer.from('KRE');

Benjamin Bouvier's avatar
Benjamin Bouvier committed
131
function encryptData(data: object, passphrase: string) {
132
    assert(process.kresus.salt !== null, 'must have provided a salt');
133

134
135
136
    const initVector = crypto.randomBytes(16);
    const key = crypto.pbkdf2Sync(passphrase, process.kresus.salt, 100000, 32, 'sha512');
    const cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, key, initVector);
137

138
139
140
141
    return Buffer.concat([
        initVector,
        ENCRYPTED_CONTENT_TAG,
        cipher.update(JSON.stringify(data)),
142
        cipher.final(),
143
    ]).toString('base64');
144
}
145

Benjamin Bouvier's avatar
Benjamin Bouvier committed
146
function decryptData(data: string, passphrase: string) {
147
148
    assert(process.kresus.salt !== null, 'must have provided a salt');

149
150
    const rawData = Buffer.from(data, 'base64');
    const [initVector, tag, encrypted] = [
151
152
        rawData.slice(0, 16),
        rawData.slice(16, 16 + 3),
153
        rawData.slice(16 + 3),
154
    ];
155
156

    if (tag.toString() !== ENCRYPTED_CONTENT_TAG.toString()) {
157
158
159
160
161
        throw new KError(
            'submitted file is not a valid kresus encrypted file',
            400,
            getErrorCode('INVALID_ENCRYPTED_EXPORT')
        );
162
163
    }

164
    const key = crypto.pbkdf2Sync(passphrase, process.kresus.salt, 100000, 32, 'sha512');
165

166
    const decipher = crypto.createDecipheriv(ENCRYPTION_ALGORITHM, key, initVector);
167
    return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString();
168
169
}

Benjamin Bouvier's avatar
Benjamin Bouvier committed
170
export async function export_(req: IdentifiedRequest<any>, res: express.Response) {
171
    try {
172
        const { id: userId } = req.user;
173

174
        let passphrase: string | null = null;
175
        if (req.body.encrypted) {
Benjamin Bouvier's avatar
Benjamin Bouvier committed
176
            if (typeof req.body.passphrase !== 'string' || !req.body.passphrase) {
177
178
179
                throw new KError('missing parameter "passphrase"', 400);
            }

180
181
182
183
184
185
186
            if (process.kresus.salt === null) {
                throw new KError(
                    "server hasn't been configured for encryption; " +
                        'please ask your administrator to provide a salt'
                );
            }

187
            passphrase = req.body.passphrase;
188
            assert(passphrase !== null, 'passphrase must be set here');
189

190
            // Check password strength
191
            if (!validatePassword(passphrase)) {
192
193
194
195
                throw new KError('submitted passphrase is too weak', 400);
            }
        }

196
        let data = await getAllData(userId, { isExport: true, cleanPassword: !passphrase });
197
        data = cleanData(data);
198

199
        let ret = {};
200
        if (passphrase) {
201
            const encryptedData = encryptData(data, passphrase);
202
203
            ret = {
                encrypted: true,
204
                data: encryptedData,
205
            };
206
        } else {
207
208
            ret = {
                encrypted: false,
209
                data,
210
            };
211
212
        }

213
        res.status(200).json(ret);
214
215
    } catch (err) {
        err.code = ERR_MSG_LOADING_ALL;
216
        asyncErr(res, err, 'when exporting data');
217
    }
218
}
219

Benjamin Bouvier's avatar
Benjamin Bouvier committed
220
221
222
type AnyObject = { [key: string]: AnyObject } | any;

function applyRenamings(model: any): (arg: AnyObject) => AnyObject {
223
224
225
    if (typeof model.renamings === 'undefined') {
        return obj => obj;
    }
Benjamin Bouvier's avatar
Benjamin Bouvier committed
226
    return (obj: AnyObject): AnyObject => {
227
228
        for (const from of Object.keys(model.renamings)) {
            const to = model.renamings[from];
229
230
231
232
233
234
235
236
237
238
239
            if (typeof obj[from] !== 'undefined') {
                if (typeof obj[to] === 'undefined') {
                    obj[to] = obj[from];
                }
                delete obj[from];
            }
        }
        return obj;
    };
}

Benjamin Bouvier's avatar
Benjamin Bouvier committed
240
export function parseDate(date: any) {
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
    let parsedDate;
    switch (typeof date) {
        case 'string':
            parsedDate = Date.parse(date);
            if (!isNaN(parsedDate)) {
                return new Date(parsedDate);
            }
            break;

        case 'number':
            if (!isNaN(date) && date > -8640000000000000 && date < 8640000000000000) {
                return new Date(date);
            }
            break;

        default:
            if (date instanceof Date) {
                return date;
            }
    }

    return null;
}

Benjamin Bouvier's avatar
Benjamin Bouvier committed
265
export async function importData(userId: number, world: any) {
Benjamin Bouvier's avatar
Benjamin Bouvier committed
266
267
268
269
270
271
272
    world.accesses = (world.accesses || []).map(applyRenamings(Access));
    world.accounts = (world.accounts || []).map(applyRenamings(Account));
    world.alerts = (world.alerts || []).map(applyRenamings(Alert));
    world.budgets = (world.budgets || []).map(applyRenamings(Budget));
    world.categories = (world.categories || []).map(applyRenamings(Category));
    world.operations = (world.operations || []).map(applyRenamings(Transaction));
    world.settings = (world.settings || []).map(applyRenamings(Setting));
273
274

    // Static data.
275
276
277
278
    world.operationtypes = world.operationtypes || [];

    // Importing only known settings prevents assertion errors in the client when
    // importing Kresus data in an older version of kresus.
Benjamin Bouvier's avatar
Benjamin Bouvier committed
279
    world.settings = world.settings.filter((s: any) => DefaultSettings.has(s.key)) || [];
280
281
282
283
284
285
286
287
288
289
290
291
292

    log.info(`Importing:
        accesses:        ${world.accesses.length}
        accounts:        ${world.accounts.length}
        alerts:          ${world.alerts.length}
        budgets:         ${world.budgets.length}
        categories:      ${world.categories.length}
        operation-types: ${world.operationtypes.length}
        settings:        ${world.settings.length}
        operations:      ${world.operations.length}
    `);

    log.info('Import accesses...');
Benjamin Bouvier's avatar
Benjamin Bouvier committed
293
    const accessMap: Remapping = {};
294
295
    for (const access of world.accesses) {
        const accessId = access.id;
296
        delete access.id;
Benjamin Bouvier's avatar
Benjamin Bouvier committed
297
298

        const sanitizedCustomFields: { name: string; value: string }[] = [];
299
300
301
302
303
304
305
306
307
308

        // Support legacy "customFields" value.
        if (typeof access.customFields === 'string' && !access.fields) {
            try {
                access.fields = JSON.parse(access.customFields);
            } catch (e) {
                log.error('Invalid JSON customFields, ignoring fields:', e.toString());
            }
        }

309
        for (const { name, value } of access.fields || []) {
310
311
312
313
314
315
316
317
318
319
320
321
322
323
            if (typeof name !== 'string') {
                log.warn('Ignoring customField because of non-string "name" property.');
                continue;
            }
            if (typeof value !== 'string') {
                log.warn(
                    `Ignoring custom field for key ${name} because of non-string "value" property`
                );
                continue;
            }
            sanitizedCustomFields.push({ name, value });
        }

        access.fields = sanitizedCustomFields;
324

325
        const created = await Access.create(userId, access);
326
327
328
329
330
331

        accessMap[accessId] = created.id;
    }
    log.info('Done.');

    log.info('Import accounts...');
Benjamin Bouvier's avatar
Benjamin Bouvier committed
332
333
    const accountIdToAccount: Map<number, number> = new Map();
    const vendorToOwnAccountId: Map<string, number> = new Map();
334
    for (const account of world.accounts) {
335
336
337
        if (typeof accessMap[account.accessId] === 'undefined') {
            log.warn('Ignoring orphan account:\n', account);
            continue;
338
339
        }

340
        const accountId = account.id;
341
342
343
344
        delete account.id;

        // For an initial import which does not come from Kresus (ex: a
        // handmade JSON file), there might be no lastCheckDate.
345
346
        account.lastCheckDate = parseDate(account.lastCheckDate);
        if (account.lastCheckDate === null) {
Benjamin Bouvier's avatar
Benjamin Bouvier committed
347
            let latestOpDate: Date | null = null;
348
            if (world.operations) {
Benjamin Bouvier's avatar
Benjamin Bouvier committed
349
                const accountOps = world.operations.filter((op: any) => op.accountId === accountId);
350
351
                for (const op of accountOps) {
                    const opDate = parseDate(op.date);
352
353
                    if (opDate !== null && (latestOpDate === null || opDate > latestOpDate)) {
                        latestOpDate = opDate;
354
355
                    }
                }
356
            }
357
            account.lastCheckDate = latestOpDate || new Date();
358
        }
359

360
        account.accessId = accessMap[account.accessId];
361
        const created = await Account.create(userId, account);
362

363
364
365
366
        accountIdToAccount.set(accountId, created.id);
        vendorToOwnAccountId.set(created.vendorAccountId, created.id);
    }
    log.info('Done.');
367

368
    log.info('Import categories...');
369
370
371
    const existingCategories = await Category.all(userId);
    const existingCategoriesMap = new Map();
    for (const category of existingCategories) {
372
373
        existingCategoriesMap.set(category.label, category);
    }
374

Benjamin Bouvier's avatar
Benjamin Bouvier committed
375
    const categoryMap: Remapping = {};
376
377
    for (const category of world.categories) {
        const catId = category.id;
378
379
        delete category.id;
        if (existingCategoriesMap.has(category.label)) {
380
            const existing = existingCategoriesMap.get(category.label);
381
382
            categoryMap[catId] = existing.id;
        } else {
383
            const created = await Category.create(userId, category);
384
            categoryMap[catId] = created.id;
385
        }
386
387
    }
    log.info('Done.');
388

389
    log.info('Import budgets...');
Benjamin Bouvier's avatar
Benjamin Bouvier committed
390
    const makeBudgetKey = (b: Budget) => `${b.categoryId}-${b.year}-${b.month}`;
391

392
393
394
    const existingBudgets = await Budget.all(userId);
    const existingBudgetsMap = new Map();
    for (const budget of existingBudgets) {
395
396
397
        existingBudgetsMap.set(makeBudgetKey(budget), budget);
    }

398
    for (const importedBudget of world.budgets) {
399
400
401
        // Note the order here: first map to the actual category id, so the
        // map lookup thereafter uses an existing category id.
        importedBudget.categoryId = categoryMap[importedBudget.categoryId];
402
        const existingBudget = existingBudgetsMap.get(makeBudgetKey(importedBudget));
403
404
405
406
407
        if (existingBudget) {
            if (
                !existingBudget.threshold ||
                existingBudget.threshold !== importedBudget.threshold
            ) {
Benjamin Bouvier's avatar
Benjamin Bouvier committed
408
                await Budget.update(userId, existingBudget.id, {
409
                    threshold: importedBudget.threshold,
410
                });
411
            }
412
413
        } else {
            delete importedBudget.id;
Benjamin Bouvier's avatar
Benjamin Bouvier committed
414
            await Budget.create(userId, importedBudget);
415
        }
416
417
    }
    log.info('Done.');
418

419
    // No need to import operation types.
420

421
    // importedTypesMap is used to set type to imported operations (backward compatibility).
422
423
424
    const importedTypes = world.operationtypes || [];
    const importedTypesMap = new Map();
    for (const type of importedTypes) {
425
426
        importedTypesMap.set(type.id.toString(), type.name);
    }
427

428
    log.info('Import transactions...');
Benjamin Bouvier's avatar
Benjamin Bouvier committed
429
    const skipTransactions: number[] = [];
430
    for (let i = 0; i < world.operations.length; i++) {
431
        const op = world.operations[i];
432

433
434
435
436
437
        op.date = parseDate(op.date);
        op.debitDate = parseDate(op.debitDate);
        op.importDate = parseDate(op.importDate);

        if (op.date === null) {
438
439
440
441
442
443
444
445
446
447
448
            log.warn('Ignoring operation without date\n', op);
            skipTransactions.push(i);
            continue;
        }

        if (typeof op.amount !== 'number' || isNaN(op.amount)) {
            log.warn('Ignoring operation without valid amount\n', op);
            skipTransactions.push(i);
            continue;
        }

449
450
451
452
        // Map operation to account.
        if (typeof op.accountId !== 'undefined') {
            if (!accountIdToAccount.has(op.accountId)) {
                log.warn('Ignoring orphan operation:\n', op);
453
                skipTransactions.push(i);
454
455
456
457
458
459
                continue;
            }
            op.accountId = accountIdToAccount.get(op.accountId);
        } else {
            if (!vendorToOwnAccountId.has(op.bankAccount)) {
                log.warn('Ignoring orphan operation:\n', op);
460
                skipTransactions.push(i);
461
                continue;
462
            }
463
            op.accountId = vendorToOwnAccountId.get(op.bankAccount);
464
465
        }

466
467
468
        // Remove bankAccount as the operation is now linked to account with accountId prop.
        delete op.bankAccount;

469
        const categoryId = op.categoryId;
470
        if (typeof categoryId !== 'undefined' && categoryId !== null) {
471
472
473
474
            if (typeof categoryMap[categoryId] === 'undefined') {
                log.warn('Unknown category, unsetting for operation:\n', op);
            }
            op.categoryId = categoryMap[categoryId];
475
        }
476

477
        // Set operation type base on operationId.
478
        if (typeof op.operationTypeID !== 'undefined') {
479
            const key = op.operationTypeID.toString();
480
481
            if (importedTypesMap.has(key)) {
                op.type = importedTypesMap.get(key);
482
            } else {
483
                op.type = UNKNOWN_OPERATION_TYPE;
484
            }
485
486
            delete op.operationTypeID;
        }
487

488
        // If there is no import date, set it to now.
489
        if (op.importDate === null) {
490
491
492
            op.importDate = new Date();
        }

493
        // If there is no label use the rawLabel, and vice-versa.
494
495
496
497
498
499
        if (typeof op.label === 'undefined') {
            op.label = op.rawLabel;
        }
        if (typeof op.rawLabel === 'undefined') {
            op.rawLabel = op.label;
        }
500
501
        if (typeof op.label === 'undefined' && typeof op.rawLabel === 'undefined') {
            log.warn('Ignoring transaction without label/rawLabel:\n', op);
502
            skipTransactions.push(i);
503
504
505
            continue;
        }

506
507
508
509
510
511
        // Consider that old imports have the type set by the user, to have a consistent behaviour
        // with the migration.
        if (typeof op.isUserDefinedType === 'undefined') {
            op.isUserDefinedType = true;
        }

512
        // Remove contents of deprecated fields, if there were any.
513
514
        delete op.attachments;
        delete op.binary;
515
516
517
518
519
520
        delete op.id;
    }
    if (skipTransactions.length) {
        for (let i = skipTransactions.length - 1; i >= 0; i--) {
            world.operations.splice(skipTransactions[i], 1);
        }
521
    }
Benjamin Bouvier's avatar
Benjamin Bouvier committed
522
    await Transaction.bulkCreate(userId, world.operations);
523
    log.info('Done.');
524

525
    log.info('Import settings...');
526
    for (const setting of world.settings) {
527
        if (ConfigGhostSettings.has(setting.key) || setting.key === 'migration-version') {
528
529
            continue;
        }
530

531
        // Reset the default account id, if it's set.
532
533
534
535
536
537
        if (
            setting.key === 'default-account-id' &&
            setting.value !== DefaultSettings.get('default-account-id')
        ) {
            if (!accountIdToAccount.has(setting.value)) {
                log.warn(`unknown default account id: ${setting.value}, skipping.`);
538
539
                continue;
            }
540
            setting.value = accountIdToAccount.get(setting.value);
541

Benjamin Bouvier's avatar
Benjamin Bouvier committed
542
            await Setting.updateByKey(userId, 'default-account-id', setting.value);
543
544
            continue;
        }
545

546
547
        // Overwrite the previous value of the demo-mode, if it was set.
        if (setting.key === 'demo-mode' && setting.value === 'true') {
548
            const found = await Setting.byKey(userId, 'demo-mode');
549
            if (found && found.value !== 'true') {
550
                await Setting.updateByKey(userId, 'demo-mode', 'true');
551
552
553
554
                continue;
            }
        }

555
        // Note that former existing values are not overwritten!
Benjamin Bouvier's avatar
Benjamin Bouvier committed
556
        await Setting.findOrCreateByKey(userId, setting.key, setting.value);
557
    }
558

559
560
561
    log.info('Done.');

    log.info('Import alerts...');
562
    for (const a of world.alerts) {
563
564
565
566
        // Map alert to account.
        if (typeof a.accountId !== 'undefined') {
            if (!accountIdToAccount.has(a.accountId)) {
                log.warn('Ignoring orphan alert:\n', a);
567
                continue;
568
            }
569
570
571
572
573
574
575
            a.accountId = accountIdToAccount.get(a.accountId);
        } else {
            if (!vendorToOwnAccountId.has(a.bankAccount)) {
                log.warn('Ignoring orphan alert:\n', a);
                continue;
            }
            a.accountId = vendorToOwnAccountId.get(a.bankAccount);
576
        }
577

578
579
        // Remove bankAccount as the alert is now linked to account with accountId prop.
        delete a.bankAccount;
580
        delete a.id;
Benjamin Bouvier's avatar
Benjamin Bouvier committed
581
        await Alert.create(userId, a);
582
583
    }
    log.info('Done.');
Nicolas Frandeboeuf's avatar
Nicolas Frandeboeuf committed
584
585
586
587

    log.info('Apply banks migrations');
    await runDataMigrations(userId);
    log.info('Done.');
588
589
}

Benjamin Bouvier's avatar
Benjamin Bouvier committed
590
export async function import_(req: IdentifiedRequest<any>, res: express.Response) {
591
    try {
592
        const { id: userId } = req.user;
593

594
595
596
597
        if (await isDemoEnabled(userId)) {
            throw new KError("importing accesses isn't allowed in demo mode", 400);
        }

598
599
        if (!req.body.data) {
            throw new KError('missing parameter "data" in the file', 400);
600
        }
601

602
603
604
605
606
607
608
        let world = req.body.data;
        if (req.body.encrypted) {
            if (typeof req.body.data !== 'string') {
                throw new KError('content of an encrypted export should be an encoded string', 400);
            }
            if (typeof req.body.passphrase !== 'string') {
                throw new KError('missing parameter "passphrase"', 400);
609
610
            }

611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
            if (process.kresus.salt === null) {
                throw new KError(
                    "server hasn't been configured for encryption; " +
                        'please ask your administrator to provide a salt'
                );
            }

            world = decryptData(world, req.body.passphrase);

            try {
                world = JSON.parse(world);
            } catch (err) {
                throw new KError(
                    'Invalid JSON file or bad passphrase.',
                    400,
                    getErrorCode('INVALID_PASSWORD_JSON_EXPORT')
                );
            }
        } else if (typeof req.body.data !== 'object') {
            throw new KError('content of a JSON export should be a JSON object', 400);
631
        }
632
633

        await importData(userId, world);
634

635
        log.info('Import finished with success!');
636
        res.status(200).end();
637
    } catch (err) {
638
        asyncErr(res, err, 'when importing data');
639
    }
640
}
641

Benjamin Bouvier's avatar
Benjamin Bouvier committed
642
export async function importOFX_(req: IdentifiedRequest<any>, res: express.Response) {
643
    try {
644
        const { id: userId } = req.user;
645
646
647
648
649
650
651
652

        log.info('Parsing OFX file...');

        await importData(userId, ofxToKresus(req.body));

        log.info('Import finished with success!');
        res.status(200).end();
    } catch (err) {
653
        asyncErr(res, err, 'when importing data');
654
655
656
    }
}

657
export const testing = {
658
659
    ofxToKresus,
    encryptData,
660
    decryptData,
661
    getAllData,
662
};