Skip to content

Commit a019eef

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into next
2 parents 59b6de2 + aff3b9e commit a019eef

File tree

3 files changed

+241
-1
lines changed

3 files changed

+241
-1
lines changed

adminforth/dataConnectors/mongo.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
189189
} else if (field.type == AdminForthDataTypes.BOOLEAN) {
190190
return value === null ? null : !!value;
191191
} else if (field.type == AdminForthDataTypes.DECIMAL) {
192+
if (value === null || value === undefined) {
193+
return null;
194+
}
192195
return value?.toString();
193196
}
194197

@@ -206,6 +209,9 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
206209
} else if (field.type == AdminForthDataTypes.BOOLEAN) {
207210
return value === null ? null : (value ? true : false);
208211
} else if (field.type == AdminForthDataTypes.DECIMAL) {
212+
if (value === null || value === undefined) {
213+
return null;
214+
}
209215
return Decimal128.fromString(value?.toString());
210216
}
211217
return value;

adminforth/documentation/docs/tutorial/07-Plugins/02-TwoFactorsAuth.md

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,238 @@ plugins: [
207207
...
208208
```
209209
210+
## Request 2FA on custom Actions
211+
212+
You might want to to allow to call some custom critical/money related actions with additional 2FA approval. This eliminates risks caused by user cookies theft by some virous/doorway software after login.
213+
214+
To do it, first, create frontend custom component which wraps and intercepts click event to menu item, and in click handler do a call to `window.adminforthTwoFaModal.getCode(cb?)` frontend API exposed by this plugin. This is awaitable call wich shows 2FA popup and asks user to authenticate with 2nd factor (if passkey is enabled it will be suggested first, with ability to fallback to TOTP)
215+
216+
```ts title='/custom/RequireTwoFaGate.vue'
217+
<template>
218+
<div class="contents" @click.stop.prevent="onClick">
219+
<slot /> <!-- render action default content - button/icon -->
220+
</div>
221+
</template>
222+
223+
<script setup lang="ts">
224+
const emit = defineEmits<{ (e: 'callAction', payload?: any): void }>();
225+
const props = defineProps<{ disabled?: boolean; meta?: Record<string, any> }>();
226+
227+
async function onClick() {
228+
if (props.disabled) return;
229+
230+
const verificationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult(); // this will ask user to enter code
231+
emit('callAction', { verificationResult }); // then we pass this verification result to action (from fronted to backend)
232+
}
233+
</script>
234+
```
235+
236+
Now we need to use verification result which we got from user on frontend, inside of backend action handler and verify that it is valid (and not expired):
237+
238+
```ts title='/adminuser.ts'
239+
options: {
240+
actions: [
241+
{
242+
name: 'Auto submit',
243+
icon: 'flowbite:play-solid',
244+
allowed: () => true,
245+
action: async ({ recordId, adminUser, adminforth, extra, cookies }) => {
246+
//diff-add
247+
const verificationResult = extra?.verificationResult
248+
//diff-add
249+
if (!verificationResult) {
250+
//diff-add
251+
return { ok: false, error: 'No verification result provided' };
252+
//diff-add
253+
}
254+
//diff-add
255+
const t2fa = adminforth.getPluginByClassName<TwoFactorsAuthPlugin>('TwoFactorsAuthPlugin');
256+
//diff-add
257+
const result = await t2fa.verify(verificationResult, {
258+
//diff-add
259+
adminUser: adminUser,
260+
//diff-add
261+
userPk: adminUser.pk,
262+
//diff-add
263+
cookies: cookies
264+
//diff-add
265+
});
266+
267+
//diff-add
268+
if (!result?.ok) {
269+
//diff-add
270+
return { ok: false, error: result?.error ?? 'Provided 2fa verification data is invalid' };
271+
//diff-add
272+
}
273+
//diff-add
274+
await adminforth
275+
//diff-add
276+
.getPluginByClassName<AuditLogPlugin>('AuditLogPlugin')
277+
//diff-add
278+
.logCustomAction({
279+
//diff-add
280+
resourceId: 'aparts',
281+
//diff-add
282+
recordId: null,
283+
//diff-add
284+
actionId: 'visitedDashboard',
285+
//diff-add
286+
oldData: null,
287+
//diff-add
288+
data: { dashboard: 'main' },
289+
//diff-add
290+
user: adminUser,
291+
//diff-add
292+
});
293+
294+
//your critical action logic
295+
296+
return { ok: true, successMessage: 'Auto submitted' };
297+
},
298+
showIn: { showButton: true, showThreeDotsMenu: true, list: true },
299+
//diff-add
300+
customComponent: '@@/RequireTwoFaGate.vue',
301+
},
302+
],
303+
}
304+
```
305+
306+
## Request 2FA from custom components
307+
308+
Imagine you have some button which does some API call
309+
310+
```ts
311+
<template>
312+
<Button @click="callAdminAPI">Call critical API</Button>
313+
</template>
314+
315+
316+
<script setup lang="ts">
317+
import { callApi } from '@/utils';
318+
import adminforth from '@/adminforth';
319+
320+
async function callAdminAPI() {
321+
const verificationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult();
322+
323+
const res = await callApi({
324+
path: '/myCriticalAction',
325+
method: 'POST',
326+
body: {
327+
param: 1
328+
},
329+
});
330+
}
331+
</script>
332+
```
333+
334+
On backend you have simple express api
335+
336+
```ts
337+
app.post(`${ADMIN_BASE_URL}/myCriticalAction`,
338+
admin.express.authorize(
339+
async (req: any, res: any) => {
340+
341+
// ... your critical logic ...
342+
343+
return res.json({ ok: true, successMessage: 'Action executed' });
344+
}
345+
)
346+
);
347+
```
348+
349+
You might want to protect this call with a second factor also. To do it, we need to make this change
350+
351+
```ts
352+
<template>
353+
<Button @click="callAdminAPI">Call critical API</Button>
354+
</template>
355+
356+
357+
<script setup lang="ts">
358+
import { callApi } from '@/utils';
359+
import adminforth from '@/adminforth';
360+
361+
async function callAdminAPI() {
362+
// diff-add
363+
const verificationResult = await window.adminforthTwoFaModal.getCode();
364+
365+
const res = await callApi({
366+
path: '/myCriticalAction',
367+
method: 'POST',
368+
body: {
369+
param: 1,
370+
// diff-add
371+
verificationResult: String(verificationResult)
372+
},
373+
});
374+
375+
// diff-add
376+
if (!res?.ok) {
377+
// diff-add
378+
adminforth.alert({ message: res.error, variant: 'danger' });
379+
// diff-add
380+
}
381+
}
382+
</script>
383+
384+
```
385+
386+
And oin API call we need to verify it:
387+
388+
389+
```ts
390+
app.post(`${ADMIN_BASE_URL}/myCriticalAction`,
391+
admin.express.authorize(
392+
async (req: any, res: any) => {
393+
394+
// diff-add
395+
const { adminUser } = req;
396+
// diff-add
397+
const { param, verificationResult } = req.body ?? {};
398+
// diff-add
399+
const t2fa = admin.getPluginByClassName<TwoFactorsAuthPlugin>('TwoFactorsAuthPlugin');
400+
// diff-add
401+
const verifyRes = await t2fa.verify(verificationResult, {
402+
// diff-add
403+
adminUser: adminUser,
404+
// diff-add
405+
userPk: adminUser.pk,
406+
// diff-add
407+
cookies: cookies
408+
// diff-add
409+
});
410+
// diff-add
411+
if (!('ok' in verifyRes)) {
412+
// diff-add
413+
return res.status(400).json({ ok: false, error: verifyRes.error || 'Verification failed' });
414+
// diff-add
415+
}
416+
// diff-add
417+
await admin.getPluginByClassName<AuditLogPlugin>('AuditLogPlugin').logCustomAction({
418+
// diff-add
419+
resourceId: 'aparts',
420+
// diff-add
421+
recordId: null,
422+
// diff-add
423+
actionId: 'myCriticalAction',
424+
// diff-add
425+
oldData: null,
426+
// diff-add
427+
data: { param },
428+
// diff-add
429+
user: adminUser,
430+
// diff-add
431+
});
432+
433+
// ... your critical logic ...
434+
435+
return res.json({ ok: true, successMessage: 'Action executed' });
436+
}
437+
)
438+
);
439+
```
440+
441+
210442
## Custom label prefix in authenticator app
211443
212444
By default label prefix in Authenticator app is formed from Adminforth [brandName setting](/docs/tutorial/Customization/branding/) which is best behaviour for most admin apps (always remember to configure brandName correctly e.g. "RoyalFinTech Admin")

adminforth/spa/src/utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@ const LS_LANG_KEY = `afLanguage`;
1313
const MAX_CONSECUTIVE_EMPTY_RESULTS = 2;
1414
const ITEMS_PER_PAGE_LIMIT = 100;
1515

16-
export async function callApi({path, method, body=undefined}: {
16+
export async function callApi({path, method, body, headers}: {
1717
path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
1818
body?: any
19+
headers?: Record<string, string>
1920
}): Promise<any> {
2021
const options = {
2122
method,
2223
headers: {
2324
'Content-Type': 'application/json',
2425
'accept-language': localStorage.getItem(LS_LANG_KEY) || 'en',
26+
...headers
2527
},
2628
body: JSON.stringify(body),
2729
};

0 commit comments

Comments
 (0)