Skip to content

Commit c95b1b6

Browse files
committed
feat: enhance call action handling to support additional payloads across components
1 parent a5defaf commit c95b1b6

File tree

7 files changed

+200
-70
lines changed

7 files changed

+200
-70
lines changed

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

Lines changed: 171 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -221,18 +221,16 @@ To do it you first need to create custom component which will call `window.admin
221221
</template>
222222

223223
<script setup lang="ts">
224-
import { callAdminForthApi } from '@/utils';
225-
const emit = defineEmits<{ (e: 'callAction'): void }>();
226-
const props = defineProps<{ disabled?: boolean; meta?: { verifyPath?: string; [k: string]: any } }>();
224+
const emit = defineEmits<{ (e: 'callAction', payload?: any): void }>();
225+
const props = defineProps<{ disabled?: boolean; meta?: Record<string, any> }>();
227226

228227
async function onClick() {
229-
if (props.disabled) {
230-
return;
231-
}
228+
if (props.disabled) return;
229+
232230
const code = await window.adminforthTwoFaModal.getCode(); // this will ask user to enter code
233231
emit('callAction', { code }); // then we pass this code to action (from fronted to backend)
234232
}
235-
</script>
233+
</script>
236234
```
237235
238236
Now we need to read code entered on fronted on backend and verify that is is valid and not expired, on backend action handler:
@@ -244,16 +242,50 @@ options: {
244242
name: 'Auto submit',
245243
icon: 'flowbite:play-solid',
246244
allowed: () => true,
247-
action: async ({ recordId, adminUser, payload, adminforth }) => {
248-
const { code } = payload;
249-
const totpIsValid = adminforth.getPluginByClassName<>('T2FAPlug').verify(code);
250-
if (!totpIsValid) {
251-
return { ok: false, error: 'TOTP code is invalid' }
252-
}
253-
// we will also register fact of ussage of this critical action using audit log Plugin
254-
getPluginBYClassName<auditlog>.logCustomAction()...
255-
.... your critical action logic ....
256-
return { ok: true, successMessage: 'Auto submitted' }
245+
action: async ({ recordId, adminUser, adminforth, extra }) => {
246+
//diff-add
247+
const code = extra?.code
248+
//diff-add
249+
if (!code) {
250+
//diff-add
251+
return { ok: false, error: 'No TOTP code provided' };
252+
//diff-add
253+
}
254+
//diff-add
255+
const t2fa = adminforth.getPluginByClassName<TwoFactorsAuthPlugin>('TwoFactorsAuthPlugin');
256+
//diff-add
257+
const result = await t2fa.verify(code, { adminUser });
258+
259+
//diff-add
260+
if (!result?.ok) {
261+
//diff-add
262+
return { ok: false, error: result?.error ?? 'TOTP code is invalid' };
263+
//diff-add
264+
}
265+
//diff-add
266+
await adminforth
267+
//diff-add
268+
.getPluginByClassName<AuditLogPlugin>('AuditLogPlugin')
269+
//diff-add
270+
.logCustomAction({
271+
//diff-add
272+
resourceId: 'aparts',
273+
//diff-add
274+
recordId: null,
275+
//diff-add
276+
actionId: 'visitedDashboard',
277+
//diff-add
278+
oldData: null,
279+
//diff-add
280+
data: { dashboard: 'main' },
281+
//diff-add
282+
user: adminUser,
283+
//diff-add
284+
});
285+
286+
//your critical action logic
287+
288+
return { ok: true, successMessage: 'Auto submitted' };
257289
},
258290
showIn: { showButton: true, showThreeDotsMenu: true, list: true },
259291
//diff-add
@@ -268,64 +300,149 @@ options: {
268300
Imagine you have some button which does some API call
269301
270302
```ts
271-
<Button @click="callApi">Call critical api</Button>
303+
<template>
304+
<Button @click="callAdminAPI">Call critical API</Button>
305+
</template>
306+
272307

308+
<script setup lang="ts">
309+
import { callApi } from '@/utils';
310+
import adminforth from '@/adminforth';
273311

274-
<script>
312+
async function callAdminAPI() {
313+
const code = await window.adminforthTwoFaModal.getCode();
275314

276-
async function callAPI() {
277-
const res = await callAdminForthAPI('/myCriticalAction', { param: 1 })
315+
const res = await callApi({
316+
path: '/myCriticalAction',
317+
method: 'POST',
318+
body: { param: 1 },
319+
});
278320
}
279-
</scrip>
321+
</script>
280322
```
281323
282324
On backend you have simple express api
283325
284-
```
326+
```ts
327+
app.post(`${ADMIN_BASE_URL}/myCriticalAction`,
328+
admin.express.authorize(
329+
async (req: any, res: any) => {
330+
331+
// ... your critical logic ...
285332

286-
app.post(
287-
adminforth.authorize(
288-
() => {
289-
req.body.param
290-
... some custom action
333+
return res.json({ ok: true, successMessage: 'Action executed' });
291334
}
292-
))
335+
)
336+
);
293337
```
294338
295339
You might want to protect this call with a TOTP code. To do it, we need to make this change
296340
297341
```ts
298-
<Button @click="callApi">Call critical api</Button>
299-
300-
301-
<script>
342+
<template>
343+
<Button @click="callAdminAPI">Call critical API</Button>
344+
</template>
345+
302346

303-
function callAPI() {
304-
// diff-remove
305-
const res = callAdminForthAPI('/myCriticalAction', { param: 1 })
306-
//diff-add
307-
const code = await window.adminforthTwoFaModal.getCode(async (code) => {
308-
//diff-add
309-
const res = await callAdminForthAPI('/myCriticalAction', { param: 1, code })
310-
//diff-add
311-
return !res.totpError
312-
}); // this will ask user to enter code
347+
<script setup lang="ts">
348+
import { callApi } from '@/utils';
349+
import adminforth from '@/adminforth';
350+
351+
async function callAdminAPI() {
352+
const code = await window.adminforthTwoFaModal.getCode();
353+
354+
// diff-remove
355+
const res = await callApi({
356+
// diff-remove
357+
path: '/myCriticalAction',
358+
// diff-remove
359+
method: 'POST',
360+
// diff-remove
361+
body: { param: 1 },
362+
// diff-remove
363+
});
364+
365+
// diff-add
366+
const res = await callApi({
367+
// diff-add
368+
path: '/myCriticalAction',
369+
// diff-add
370+
method: 'POST',
371+
// diff-add
372+
body: { param: 1, code: String(code) },
373+
// diff-add
374+
});
375+
376+
// diff-add
377+
if (!res?.ok) {
378+
// diff-add
379+
adminforth.alert({ message: res.error, variant: 'danger' });
380+
// diff-add
381+
}
313382
}
314-
</scrip>
383+
</script>
384+
315385
```
316386
317387
And oin API call we need to verify it:
318388
319389
320-
```
321-
322-
app.post(
323-
adminforth.authorize(
324-
() => {
325-
//diff-add
326-
getPBCNM
327-
.. log some critical action
328-
... some custom action
390+
```ts
391+
app.post(`${ADMIN_BASE_URL}/myCriticalAction`,
392+
admin.express.authorize(
393+
async (req: any, res: any) => {
394+
395+
// diff-remove
396+
// ... your critical logic ...
397+
398+
// diff-remove
399+
return res.json({ ok: true, successMessage: 'Action executed' });
400+
401+
// diff-add
402+
const { adminUser } = req;
403+
// diff-add
404+
const { param, code } = req.body ?? {};
405+
// diff-add
406+
const token = String(code ?? '').replace(/\D/g, '');
407+
// diff-add
408+
if (token.length !== 6) {
409+
// diff-add
410+
return res.status(401).json({ ok: false, error: 'TOTP must be 6 digits' });
411+
// diff-add
412+
}
413+
// diff-add
414+
const t2fa = admin.getPluginByClassName<TwoFactorsAuthPlugin>('TwoFactorsAuthPlugin');
415+
// diff-add
416+
const verifyRes = await t2fa.verify(token, { adminUser });
417+
// diff-add
418+
if (!('ok' in verifyRes)) {
419+
// diff-add
420+
return res.status(400).json({ ok: false, error: verifyRes.error || 'Wrong or expired OTP code' });
421+
// diff-add
422+
}
423+
// diff-add
424+
await admin.getPluginByClassName<AuditLogPlugin>('AuditLogPlugin').logCustomAction({
425+
// diff-add
426+
resourceId: 'aparts',
427+
// diff-add
428+
recordId: null,
429+
// diff-add
430+
actionId: 'myCriticalAction',
431+
// diff-add
432+
oldData: null,
433+
// diff-add
434+
data: { param },
435+
// diff-add
436+
user: adminUser,
437+
// diff-add
438+
});
439+
440+
// diff-add
441+
// ... your critical logic ...
442+
443+
// diff-add
444+
return res.json({ ok: true, successMessage: 'Action executed' });
329445
}
330-
))
446+
)
447+
);
331448
```

adminforth/modules/codeInjector.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,19 @@ class CodeInjector implements ICodeInjector {
452452
});
453453
}
454454
});
455+
resource.options.actions.forEach((action) => {
456+
const cc = action.customComponent;
457+
if (!cc) return;
458+
459+
const file = cc.file;
460+
if (!file) {
461+
throw new Error('customComponent.file is missing for action: ' + JSON.stringify({ id: action.id, name: action.name }));
462+
}
463+
if (!customResourceComponents.includes(file)) {
464+
console.log('Found injection', file);
465+
customResourceComponents.push(file);
466+
}
467+
});
455468

456469
(Object.values(resource.options?.pageInjections || {})).forEach((injection) => {
457470
Object.values(injection).forEach((filePathes: {file: string}[]) => {

adminforth/modules/restApi.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,7 +1423,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
14231423
method: 'POST',
14241424
path: '/start_custom_action',
14251425
handler: async ({ body, adminUser, tr }) => {
1426-
const { resourceId, actionId, recordId } = body;
1426+
const { resourceId, actionId, recordId, extra } = body;
14271427
const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
14281428
if (!resource) {
14291429
return { error: await tr(`Resource {resourceId} not found`, 'errors', { resourceId }) };
@@ -1454,7 +1454,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
14541454
redirectUrl: action.url
14551455
}
14561456
}
1457-
const response = await action.action({ recordId, adminUser, resource, tr, adminforth: this.adminforth });
1457+
const response = await action.action({ recordId, adminUser, resource, tr, adminforth: this.adminforth, extra });
14581458

14591459
return {
14601460
actionId,

adminforth/spa/src/components/ResourceListTable.vue

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,21 +180,18 @@
180180
v-for="action in resource.options.actions.filter(a => a.showIn?.list)"
181181
:key="action.id"
182182
>
183-
<CallActionWrapper
184-
:disabled="rowActionLoadingStates?.[action.id]"
185-
@callAction="startCustomAction(action.id, row)"
186-
>
187183
<component
188-
:is="action.customComponent ? getCustomComponent(action.customComponent) : 'span'"
184+
:is="action.customComponent ? getCustomComponent(action.customComponent) : CallActionWrapper"
189185
:meta="action.customComponent?.meta"
190186
:row="row"
191187
:resource="resource"
192188
:adminUser="adminUser"
189+
@callAction="(payload? : Object) => startCustomAction(action.id, payload ?? row)"
193190
>
194191
<button
195192
type="button"
196193
:disabled="rowActionLoadingStates?.[action.id]"
197-
@click.stop.prevent="startCustomAction(action.id, row)"
194+
@click.stop.prevent
198195
>
199196
<component
200197
v-if="action.icon"
@@ -203,14 +200,14 @@
203200
/>
204201
</button>
205202
</component>
206-
</CallActionWrapper>
207203

208204
<template #tooltip>
209205
{{ action.name }}
210206
</template>
211207
</Tooltip>
212208
</template>
213209
</div>
210+
214211
</td>
215212
</tr>
216213
</tbody>

adminforth/spa/src/components/ResourceListTableVirtual.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,12 @@
200200
:row="row"
201201
:resource="resource"
202202
:adminUser="adminUser"
203+
@callAction="(payload? : Object) => startCustomAction(action.id, payload ?? row)"
203204
>
204205
<button
205206
type="button"
206207
:disabled="rowActionLoadingStates?.[action.id]"
207-
@click.stop.prevent="startCustomAction(action.id, row)"
208+
@click.stop.prevent
208209
>
209210
<component
210211
v-if="action.icon"

adminforth/spa/src/components/ThreeDotsMenu.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<component
2828
:is="(action.customComponent && getCustomComponent(action.customComponent)) || CallActionWrapper"
2929
:meta="action.customComponent?.meta"
30-
@callAction="handleActionClick(action)"
30+
@callAction="(payload? : Object) => handleActionClick(action, payload)"
3131
>
3232
<a href="#" @click.prevent class="block px-4 py-2 hover:text-lightThreeDotsMenuBodyTextHover hover:bg-lightThreeDotsMenuBodyBackgroundHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover">
3333
<div class="flex items-center gap-2">
@@ -87,7 +87,7 @@ const props = defineProps({
8787
8888
const emit = defineEmits(['startBulkAction']);
8989
90-
async function handleActionClick(action) {
90+
async function handleActionClick(action: any, payload: any) {
9191
adminforth.list.closeThreeDotsDropdown();
9292
9393
const actionId = action.id;
@@ -97,7 +97,8 @@ async function handleActionClick(action) {
9797
body: {
9898
resourceId: route.params.resourceId,
9999
actionId: actionId,
100-
recordId: route.params.primaryKey
100+
recordId: route.params.primaryKey,
101+
extra: payload || {},
101102
}
102103
});
103104

0 commit comments

Comments
 (0)