Skip to content

Commit 24e3491

Browse files
committed
fix(admin): admin can fail and fix decryption
1 parent de50c06 commit 24e3491

File tree

4 files changed

+94
-24
lines changed

4 files changed

+94
-24
lines changed

front/public/locales/fr/common.json.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,7 @@ export default {
5353
'admin.placeholder_debtor_addr_2': 'Adresse du débiteur (ligne 2)',
5454
'admin.placeholder_configure': 'Configurer',
5555
'admin.configure': "Editer la configuration de l'export",
56+
'admin.error_decrypt':
57+
"Impossible de déchiffrer les données. Vérifie la clé privée et télécharge l'original avant de fermer l'onglet si l'erreur ne disparait pas.",
5658
'legals.back': 'Retourner au site',
5759
} as const;

front/src/api/api.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ type RawResponseType<T> = T extends Date
3434
}
3535
: T;
3636

37+
type APIError = {
38+
errorCode: number;
39+
error: string;
40+
};
41+
3742
/**
3843
* The response handler is a class that allows you to handle the response of a request to the API.
3944
* It allows you to define what to do when the request is successful, when it returns an error (an API error), when it fails (a request error), or when it returns a specific status code or specific failure.
@@ -62,7 +67,7 @@ export class ResponseHandler<T, R = undefined> {
6267
private readonly handlers: { [status: number]: (body: T) => R | void } & Partial<{
6368
[status in ResponseError | 'success' | 'error' | 'failure']: status extends 'success'
6469
? (body: T) => R | void
65-
: () => R | void;
70+
: (...args: status extends 'error' ? [error: APIError] : []) => R | void;
6671
}> = {};
6772
private readonly promise: Promise<R | void>;
6873

@@ -81,15 +86,19 @@ export class ResponseHandler<T, R = undefined> {
8186
if (response.code < 400) {
8287
return 'success' in this.handlers ? this.handlers.success!(response.body) : undefined;
8388
}
84-
return 'error' in this.handlers ? this.handlers.error!() : undefined;
89+
return 'error' in this.handlers ? this.handlers.error!(response.body as APIError) : undefined;
8590
});
8691
}
8792

8893
on<E, P extends number | ResponseError | 'success' | 'error' | 'failure'>(
8994
statusCode: P,
90-
handler: P extends number | 'success' ? (body: T) => E | void : () => E | void,
95+
handler: P extends number | 'success'
96+
? (body: T) => E | void
97+
: P extends 'error'
98+
? (error: APIError) => E | void
99+
: () => E | void,
91100
): ResponseHandler<T, R | E> {
92-
this.handlers[statusCode] = handler as (body?: T) => R | void;
101+
this.handlers[statusCode] = handler as (body?: T | APIError) => R | void;
93102
return this;
94103
}
95104

@@ -179,7 +188,8 @@ async function internalRequestAPI<RequestType, ResponseType>(
179188
if (response.status === StatusCodes.NO_CONTENT) {
180189
return { code: response.status, body: null as ResponseType };
181190
}
182-
if (isFile && method === 'GET') return { code: response.status, body: await response.text() };
191+
if (isFile && method === 'GET' && response.status < StatusCodes.BAD_REQUEST)
192+
return { code: response.status, body: await response.text() };
183193
if (!response.headers.get('content-type')?.includes('application/json')) return { error: ResponseError.not_json };
184194

185195
try {

front/src/app/admin/page.tsx

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import styles from './style.module.scss';
4-
import { useEffect, useState } from 'react';
4+
import { useEffect, useRef, useState } from 'react';
55
import { useAppTranslation } from '@/lib/i18n';
66
import { usePageSettings } from '@/module/pageSettings';
77
import { useConnectedUser } from '@/module/user';
@@ -52,24 +52,6 @@ async function decryptData(data: string, pemEncodedKey: string): Promise<string>
5252
return String.fromCodePoint(...new Uint8Array(buffer));
5353
}
5454

55-
async function downloadReport(api: API, privateKey: string) {
56-
api.get<string>('/admin/report', { isFile: true }).on('success', async (xml) => {
57-
let xmlString = xml;
58-
const encrypted = xml.matchAll(/(?<=<(?<tag>[^<>]+?)>)[^<>]{100,}(?=<\/\k<tag>>)/g);
59-
for (const data of encrypted) {
60-
const realWorldData = await decryptData(data[0], privateKey);
61-
xmlString = xmlString.replaceAll(data[0], realWorldData);
62-
}
63-
const blob = new Blob([xmlString], { type: 'application/xml' });
64-
const url = URL.createObjectURL(blob);
65-
const a = document.createElement('a');
66-
a.href = url;
67-
a.download = `buckutt-report-${Date.now()}.xml`;
68-
a.click();
69-
URL.revokeObjectURL(url);
70-
});
71-
}
72-
7355
type ConfigurationStatus = {
7456
debtor_iban: boolean;
7557
debtor_bic: boolean;
@@ -96,7 +78,20 @@ export default function AdminPage() {
9678
debtor_iban: false,
9779
debtor_name: false,
9880
});
81+
const [error, setError] = useState('');
9982
const [hasSettingsOpen, setSettingsOpen] = useState(false);
83+
const downloadedData = useRef('');
84+
const [downloadedDataLink, setDownloadedDataLink] = useState('');
85+
86+
useEffect(() => {
87+
if (!downloadedData.current) setDownloadedDataLink('');
88+
else {
89+
const blob = new Blob([downloadedData.current], { type: 'application/xml' });
90+
const url = URL.createObjectURL(blob);
91+
setDownloadedDataLink(url);
92+
return () => URL.revokeObjectURL(url);
93+
}
94+
}, [downloadedData.current]);
10095

10196
useEffect(() => {
10297
api.get<ConfigurationStatus>('/admin/config').on('success', (data) => {
@@ -127,6 +122,40 @@ export default function AdminPage() {
127122
});
128123
};
129124

125+
const parseReport = async (xml: string, privateKey: string) => {
126+
let xmlString = xml;
127+
const encrypted = xml.matchAll(/(?<=<(?<tag>[^<>]+?)>)[^<>]{100,}(?=<\/\k<tag>>)/g);
128+
try {
129+
for (const data of encrypted) {
130+
const realWorldData = await decryptData(data[0], privateKey);
131+
xmlString = xmlString.replaceAll(data[0], realWorldData);
132+
}
133+
} catch {
134+
downloadedData.current = xml;
135+
setError(t('common:admin.error_decrypt'));
136+
return;
137+
}
138+
setError('');
139+
const blob = new Blob([xmlString], { type: 'application/xml' });
140+
const url = URL.createObjectURL(blob);
141+
const a = document.createElement('a');
142+
a.href = url;
143+
a.download = `buckutt-report-${Date.now()}.xml`;
144+
a.click();
145+
URL.revokeObjectURL(url);
146+
};
147+
148+
const downloadReport = async (api: API, privateKey: string) => {
149+
if (downloadedData.current) parseReport(downloadedData.current, privateKey);
150+
else
151+
api
152+
.get<string>('/admin/report', { isFile: true })
153+
.on('success', (xml) => parseReport(xml, privateKey))
154+
.on('error', ({ error }) => {
155+
setError(error);
156+
});
157+
};
158+
130159
return (
131160
<AppModal>
132161
<div className={styles.title}>
@@ -136,6 +165,21 @@ export default function AdminPage() {
136165
</span>{' '}
137166
🐩
138167
</div>
168+
{error ? (
169+
<div className={styles.warn}>
170+
{error === t('common:admin.error_decrypt')
171+
? [
172+
error.slice(0, error.indexOf('.') + 2),
173+
<a href={downloadedDataLink} download={`buckutt-raw-${Date.now()}.xml`} key="link">
174+
{error.slice(error.indexOf('.') + 2, error.indexOf('.', error.indexOf('.') + 1))}
175+
</a>,
176+
error.slice(error.indexOf('.', error.indexOf('.') + 1)),
177+
]
178+
: error}
179+
</div>
180+
) : (
181+
<></>
182+
)}
139183
{hasLoaded ? (
140184
hasSettingsOpen ? (
141185
<div className={styles.margin}>

front/src/app/admin/style.module.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,17 @@
4242
vertical-align: text-bottom;
4343
}
4444
}
45+
46+
.warn {
47+
display: block;
48+
@include box-error;
49+
padding: 0.5em;
50+
border-radius: 5px;
51+
border: 1px solid currentColor;
52+
margin-top: 20px;
53+
54+
a {
55+
text-decoration: none;
56+
border-bottom: 1px solid currentColor;
57+
}
58+
}

0 commit comments

Comments
 (0)