@autobusal/admin-api-consumers 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Browse.tsx +93 -0
- package/CHANGELOG.md +20 -0
- package/Manage.tsx +178 -0
- package/TokenManager.tsx +105 -0
- package/TokenRevealModal.tsx +67 -0
- package/index.ts +7 -0
- package/package.json +7 -0
- package/services.ts +146 -0
- package/styles.ts +31 -0
- package/types.ts +19 -0
package/Browse.tsx
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
2
|
+
import { useSearchParams } from 'react-router-dom';
|
|
3
|
+
import { Meta, Table, Row } from '@autobusal/common';
|
|
4
|
+
import { usePage, useBreadcrumbs } from '@autobusal/hooks';
|
|
5
|
+
import { DisplayStatus, getParams, success } from '@autobusal/utilities';
|
|
6
|
+
import { useGetApiConsumers, useDeleteApiConsumer } from './services';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
url: string
|
|
10
|
+
t: TFunction<'common'>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const Browse = ({ url, t }: Props): JSX.Element => {
|
|
14
|
+
const [ searchParams ] = useSearchParams();
|
|
15
|
+
|
|
16
|
+
usePage('api_consumers');
|
|
17
|
+
|
|
18
|
+
useBreadcrumbs(t('admin_api_consumers.browse.title', { ns: 'common' }));
|
|
19
|
+
|
|
20
|
+
const { data, isLoading, isFetching, refetch } = useGetApiConsumers(getParams(searchParams, ['page', 'sort', 'order', 'q']));
|
|
21
|
+
|
|
22
|
+
const { mutate: Delete } = useDeleteApiConsumer();
|
|
23
|
+
|
|
24
|
+
const onDelete = (id: number): void => {
|
|
25
|
+
Delete(id, {
|
|
26
|
+
onSuccess: () => {
|
|
27
|
+
success(t('admin_api_consumers.manage.messages.deleted', { ns: 'common' }));
|
|
28
|
+
|
|
29
|
+
refetch();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const rows = data?.data.map(item => (
|
|
35
|
+
<Row
|
|
36
|
+
key={ item.id }
|
|
37
|
+
id={ item.id }
|
|
38
|
+
data={ [
|
|
39
|
+
item.company,
|
|
40
|
+
item.user.email,
|
|
41
|
+
item.operators.includes('all') ? t('admin_api_consumers.browse.all_operators', { ns: 'common' }) : String(item.operators.length),
|
|
42
|
+
item.external_providers.length > 0 ? item.external_providers.join(', ') : '-',
|
|
43
|
+
DisplayStatus(item.user.status, t)
|
|
44
|
+
] }
|
|
45
|
+
/>
|
|
46
|
+
));
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Meta title={ t('admin_api_consumers.browse.title', { ns: 'common' }) }>
|
|
50
|
+
<h1>{ t('admin_api_consumers.browse.title', { ns: 'common' }) }</h1>
|
|
51
|
+
|
|
52
|
+
<Table
|
|
53
|
+
url={ url }
|
|
54
|
+
columns={ [{
|
|
55
|
+
name: t('admin_api_consumers.browse.company', { ns: 'common' }),
|
|
56
|
+
width: 25,
|
|
57
|
+
slug: 'company',
|
|
58
|
+
type: 'az'
|
|
59
|
+
}, {
|
|
60
|
+
name: t('admin_api_consumers.browse.email', { ns: 'common' }),
|
|
61
|
+
width: 25
|
|
62
|
+
}, {
|
|
63
|
+
name: t('admin_api_consumers.browse.operators', { ns: 'common' }),
|
|
64
|
+
width: 18
|
|
65
|
+
}, {
|
|
66
|
+
name: t('admin_api_consumers.browse.external_providers', { ns: 'common' }),
|
|
67
|
+
width: 18
|
|
68
|
+
}, {
|
|
69
|
+
name: t('admin_api_consumers.browse.status', { ns: 'common' }),
|
|
70
|
+
width: 14
|
|
71
|
+
}] }
|
|
72
|
+
rows={ rows }
|
|
73
|
+
pages={ data }
|
|
74
|
+
sorting={ {
|
|
75
|
+
slug: 'company',
|
|
76
|
+
order: 'asc'
|
|
77
|
+
} }
|
|
78
|
+
search={ searchParams.get('q') }
|
|
79
|
+
loading={ isLoading }
|
|
80
|
+
fetching={ isFetching }
|
|
81
|
+
t={ t }
|
|
82
|
+
actions={ [
|
|
83
|
+
'create', 'search', 'view', 'delete'
|
|
84
|
+
] }
|
|
85
|
+
handlers={ {
|
|
86
|
+
delete: onDelete
|
|
87
|
+
} }
|
|
88
|
+
/>
|
|
89
|
+
</Meta>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default Browse;
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@autobusal/admin-api-consumers` are documented here.
|
|
4
|
+
This project adheres to [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
5
|
+
and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [1.0.0] - 2026-07-27
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Initial release. Admin GUI for third-party API Consumers (agent-type
|
|
12
|
+
users authenticating with a Sanctum token against `/api/partners/*`),
|
|
13
|
+
replacing the artisan-only `make:user_api` command (obtapi
|
|
14
|
+
`App\Http\Controllers\ApiConsumers\AdminController`). Create with
|
|
15
|
+
one-time token reveal, edit company/status/operators allowlist/external-
|
|
16
|
+
providers allowlist/commission rates, enable-disable without touching
|
|
17
|
+
the token, revoke, and regenerate (old token invalidated, new one
|
|
18
|
+
revealed once). Deliberately relaxed validation vs the regular agents
|
|
19
|
+
admin - no nipt/license/address/mobile/subscription required, since an
|
|
20
|
+
API consumer has no storefront presence.
|
package/Manage.tsx
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { TFunction } from 'i18next';
|
|
3
|
+
import { useParams, useNavigate } from 'react-router-dom';
|
|
4
|
+
import { Meta, BackWithTitle, Viewer, Box } from '@autobusal/common';
|
|
5
|
+
import { usePage, useBreadcrumbs, useOperators } from '@autobusal/hooks';
|
|
6
|
+
import { GetStatus, getAllowed, success } from '@autobusal/utilities';
|
|
7
|
+
import { useGetApiConsumer, usePostApiConsumer, useDeleteApiConsumer, useGetProviders } from './services';
|
|
8
|
+
import TokenManager from './TokenManager';
|
|
9
|
+
import TokenRevealModal from './TokenRevealModal';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
url: string
|
|
13
|
+
t: TFunction<'common'>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const Manage = ({ url, t }: Props): JSX.Element => {
|
|
17
|
+
const params = useParams();
|
|
18
|
+
|
|
19
|
+
const id = Number(params.id);
|
|
20
|
+
|
|
21
|
+
usePage('api_consumers');
|
|
22
|
+
|
|
23
|
+
const navigate = useNavigate();
|
|
24
|
+
|
|
25
|
+
const [ revealToken, setRevealToken ] = useState<string | null>(null);
|
|
26
|
+
|
|
27
|
+
const operators = useOperators();
|
|
28
|
+
|
|
29
|
+
const { data: providers, isLoading: isLoadingProviders } = useGetProviders();
|
|
30
|
+
|
|
31
|
+
const { data, isFetching } = useGetApiConsumer(id);
|
|
32
|
+
|
|
33
|
+
const title = id > 0 ? t('admin_api_consumers.manage.title.update', { company: data?.company, ns: 'common' }) : t('admin_api_consumers.manage.title.new', { ns: 'common' });
|
|
34
|
+
|
|
35
|
+
useBreadcrumbs(title, [{ name: t('admin_api_consumers.browse.title', { ns: 'common' }), url }]);
|
|
36
|
+
|
|
37
|
+
const { mutate: Update, isPending: isPendingSave } = usePostApiConsumer(id);
|
|
38
|
+
|
|
39
|
+
const { mutate: Delete, isPending: isPendingDelete } = useDeleteApiConsumer();
|
|
40
|
+
|
|
41
|
+
if (isLoadingProviders) {
|
|
42
|
+
return <Box />;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const onSave = (formData: Record<string, any>): void => {
|
|
46
|
+
Update(formData, {
|
|
47
|
+
onSuccess: (response) => {
|
|
48
|
+
success(t('admin_api_consumers.manage.messages.saved', { ns: 'common' }));
|
|
49
|
+
|
|
50
|
+
// one-time token reveal on create - hold the navigation until the
|
|
51
|
+
// admin has seen/copied it, since it can never be shown again
|
|
52
|
+
if (response.api_token) {
|
|
53
|
+
setRevealToken(response.api_token);
|
|
54
|
+
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
navigate(url);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const onDelete = (): void => {
|
|
64
|
+
Delete(id, {
|
|
65
|
+
onSuccess: () => {
|
|
66
|
+
success(t('admin_api_consumers.manage.messages.deleted', { ns: 'common' }));
|
|
67
|
+
|
|
68
|
+
navigate(url);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Meta title={ title }>
|
|
75
|
+
<BackWithTitle title={ title } to={ url } t={ t } />
|
|
76
|
+
|
|
77
|
+
<Viewer
|
|
78
|
+
id={ id }
|
|
79
|
+
data={ [{
|
|
80
|
+
label: t('admin_api_consumers.manage.company', { ns: 'common' }),
|
|
81
|
+
name: 'company',
|
|
82
|
+
type: 'text',
|
|
83
|
+
value: data?.company,
|
|
84
|
+
rules: 'required|min_length:2|max_length:255'
|
|
85
|
+
}, {}, {
|
|
86
|
+
label: t('admin_api_consumers.manage.email', { ns: 'common' }),
|
|
87
|
+
name: 'email',
|
|
88
|
+
type: 'email',
|
|
89
|
+
value: data?.user.email,
|
|
90
|
+
rules: id === 0 ? 'required' : undefined
|
|
91
|
+
}, {
|
|
92
|
+
label: t('admin_api_consumers.manage.password', { ns: 'common' }),
|
|
93
|
+
name: 'password',
|
|
94
|
+
type: 'password',
|
|
95
|
+
rules: id === 0 ? 'required|min_length:6' : undefined
|
|
96
|
+
}, {}, {
|
|
97
|
+
label: t('admin_api_consumers.manage.operators', { ns: 'common' }),
|
|
98
|
+
name: 'operators',
|
|
99
|
+
type: 'checkbox-list-all',
|
|
100
|
+
selected: data?.operators,
|
|
101
|
+
values: operators
|
|
102
|
+
}, {
|
|
103
|
+
label: t('admin_api_consumers.manage.external_providers', { ns: 'common' }),
|
|
104
|
+
name: 'external_providers',
|
|
105
|
+
type: 'checkbox-list-all',
|
|
106
|
+
selected: data?.external_providers,
|
|
107
|
+
values: providers
|
|
108
|
+
}, {}, {
|
|
109
|
+
label: t('admin_api_consumers.manage.departure', { ns: 'common' }),
|
|
110
|
+
name: 'departure',
|
|
111
|
+
type: 'text',
|
|
112
|
+
value: data?.user.comissions?.departure_edit
|
|
113
|
+
}, {
|
|
114
|
+
label: t('admin_api_consumers.manage.departure_self', { ns: 'common' }),
|
|
115
|
+
name: 'departure_self',
|
|
116
|
+
type: 'text',
|
|
117
|
+
value: data?.user.comissions?.departure_self_edit
|
|
118
|
+
}, {}, {
|
|
119
|
+
label: t('admin_api_consumers.manage.return', { ns: 'common' }),
|
|
120
|
+
name: 'return',
|
|
121
|
+
type: 'text',
|
|
122
|
+
value: data?.user.comissions?.return_edit
|
|
123
|
+
}, {
|
|
124
|
+
label: t('admin_api_consumers.manage.return_self', { ns: 'common' }),
|
|
125
|
+
name: 'return_self',
|
|
126
|
+
type: 'text',
|
|
127
|
+
value: data?.user.comissions?.return_self_edit
|
|
128
|
+
}, {}, {
|
|
129
|
+
label: t('admin_api_consumers.manage.unique', { ns: 'common' }),
|
|
130
|
+
name: 'unique',
|
|
131
|
+
type: 'select',
|
|
132
|
+
value: data?.user.comissions?.unique ?? 0,
|
|
133
|
+
values: getAllowed(t)
|
|
134
|
+
}, {
|
|
135
|
+
label: t('admin_api_consumers.manage.status', { ns: 'common' }),
|
|
136
|
+
name: 'status',
|
|
137
|
+
type: 'select',
|
|
138
|
+
value: data?.user.status ?? 1,
|
|
139
|
+
values: GetStatus(t)
|
|
140
|
+
}, {}, ...(id > 0 ? [{
|
|
141
|
+
label: t('admin_api_consumers.manage.token', { ns: 'common' }),
|
|
142
|
+
name: 'token_manager',
|
|
143
|
+
type: 'component' as const,
|
|
144
|
+
value: (
|
|
145
|
+
<TokenManager
|
|
146
|
+
id={ id }
|
|
147
|
+
hasToken={ data?.user.api?.has_token ?? false }
|
|
148
|
+
enabled={ data?.user.api?.api ?? 0 }
|
|
149
|
+
t={ t }
|
|
150
|
+
/>
|
|
151
|
+
)
|
|
152
|
+
}] : [])] }
|
|
153
|
+
fetching={ isFetching }
|
|
154
|
+
pending={ isPendingSave || isPendingDelete }
|
|
155
|
+
t={ t }
|
|
156
|
+
actions={
|
|
157
|
+
id > 0 ? ['save', 'delete'] : ['save']
|
|
158
|
+
}
|
|
159
|
+
onSave={ onSave }
|
|
160
|
+
onDelete={ onDelete }
|
|
161
|
+
/>
|
|
162
|
+
|
|
163
|
+
{ revealToken && (
|
|
164
|
+
<TokenRevealModal
|
|
165
|
+
token={ revealToken }
|
|
166
|
+
t={ t }
|
|
167
|
+
onClose={ () => {
|
|
168
|
+
setRevealToken(null);
|
|
169
|
+
|
|
170
|
+
navigate(url);
|
|
171
|
+
} }
|
|
172
|
+
/>
|
|
173
|
+
) }
|
|
174
|
+
</Meta>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export default Manage;
|
package/TokenManager.tsx
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { TFunction } from 'i18next';
|
|
3
|
+
import { FiRefreshCw, FiSlash } from 'react-icons/fi';
|
|
4
|
+
import { Button } from '@autobusal/common';
|
|
5
|
+
import { success } from '@autobusal/utilities';
|
|
6
|
+
import { TokenStatus, TokenActions } from './styles';
|
|
7
|
+
import TokenRevealModal from './TokenRevealModal';
|
|
8
|
+
import { useToggleApiConsumer, useRevokeApiConsumerToken, useRegenerateApiConsumerToken } from './services';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
id: number
|
|
12
|
+
hasToken: boolean
|
|
13
|
+
enabled: number
|
|
14
|
+
t: TFunction<'common'>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-27
|
|
18
|
+
// Embedded widget (same convention as ApiDetails/IpLogs in the shared
|
|
19
|
+
// agents-manage form) for the token lifecycle - kept separate from the
|
|
20
|
+
// main Viewer save action since these are immediate, independent
|
|
21
|
+
// operations (toggling/revoking/regenerating shouldn't require also
|
|
22
|
+
// saving the rest of the form).
|
|
23
|
+
const TokenManager = ({ id, hasToken, enabled, t }: Props): JSX.Element => {
|
|
24
|
+
const [ reveal, setReveal ] = useState<string | null>(null);
|
|
25
|
+
|
|
26
|
+
const { mutate: Toggle, isPending: isPendingToggle } = useToggleApiConsumer();
|
|
27
|
+
const { mutate: Revoke, isPending: isPendingRevoke } = useRevokeApiConsumerToken();
|
|
28
|
+
const { mutate: Regenerate, isPending: isPendingRegenerate } = useRegenerateApiConsumerToken();
|
|
29
|
+
|
|
30
|
+
const onToggle = (): void => {
|
|
31
|
+
Toggle({ id, api: enabled === 1 ? 0 : 1 }, {
|
|
32
|
+
onSuccess: () => success(t('admin_api_consumers.token.messages.updated', { ns: 'common' }))
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const onRevoke = (): void => {
|
|
37
|
+
if (!window.confirm(t('admin_api_consumers.token.confirm_revoke', { ns: 'common' }))) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Revoke(id, {
|
|
42
|
+
onSuccess: () => success(t('admin_api_consumers.token.messages.revoked', { ns: 'common' }))
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const onRegenerate = (): void => {
|
|
47
|
+
if (hasToken && !window.confirm(t('admin_api_consumers.token.confirm_regenerate', { ns: 'common' }))) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Regenerate(id, {
|
|
52
|
+
onSuccess: (data) => setReveal(data.api_token)
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
<TokenStatus>
|
|
59
|
+
{ hasToken
|
|
60
|
+
? t('admin_api_consumers.token.has_token', { ns: 'common' })
|
|
61
|
+
: t('admin_api_consumers.token.no_token', { ns: 'common' }) }
|
|
62
|
+
|
|
63
|
+
<Button
|
|
64
|
+
type="button"
|
|
65
|
+
size="small"
|
|
66
|
+
subtype={ enabled === 1 ? 'delete' : undefined }
|
|
67
|
+
noMargin
|
|
68
|
+
loading={ isPendingToggle }
|
|
69
|
+
text={ t(enabled === 1 ? 'admin_api_consumers.token.disable' : 'admin_api_consumers.token.enable', { ns: 'common' }) }
|
|
70
|
+
onClick={ onToggle }
|
|
71
|
+
/>
|
|
72
|
+
</TokenStatus>
|
|
73
|
+
|
|
74
|
+
<TokenActions>
|
|
75
|
+
<Button
|
|
76
|
+
type="button"
|
|
77
|
+
size="small"
|
|
78
|
+
subtype="secondary"
|
|
79
|
+
noMargin
|
|
80
|
+
loading={ isPendingRegenerate }
|
|
81
|
+
text={ <><FiRefreshCw /> { t('admin_api_consumers.token.regenerate', { ns: 'common' }) }</> }
|
|
82
|
+
onClick={ onRegenerate }
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
{ hasToken && (
|
|
86
|
+
<Button
|
|
87
|
+
type="button"
|
|
88
|
+
size="small"
|
|
89
|
+
subtype="delete"
|
|
90
|
+
noMargin
|
|
91
|
+
loading={ isPendingRevoke }
|
|
92
|
+
text={ <><FiSlash /> { t('admin_api_consumers.token.revoke', { ns: 'common' }) }</> }
|
|
93
|
+
onClick={ onRevoke }
|
|
94
|
+
/>
|
|
95
|
+
) }
|
|
96
|
+
</TokenActions>
|
|
97
|
+
|
|
98
|
+
{ reveal && (
|
|
99
|
+
<TokenRevealModal token={ reveal } t={ t } onClose={ () => setReveal(null) } />
|
|
100
|
+
) }
|
|
101
|
+
</>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export default TokenManager;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { TFunction } from 'i18next';
|
|
3
|
+
import { FiCopy, FiCheck } from 'react-icons/fi';
|
|
4
|
+
import { Modal, Button } from '@autobusal/common';
|
|
5
|
+
import { TokenBox, TokenValue } from './styles';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
token: string
|
|
9
|
+
t: TFunction<'common'>
|
|
10
|
+
onClose: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-27
|
|
14
|
+
// One-time token reveal - nothing in this codebase re-displays a Sanctum
|
|
15
|
+
// token after creation (obtapi never re-selects `users_api.token`), so
|
|
16
|
+
// this is the ONLY place a consumer's token is ever shown. Closing this
|
|
17
|
+
// modal without copying it means it's gone for good (regenerate is the
|
|
18
|
+
// only recovery).
|
|
19
|
+
const TokenRevealModal = ({ token, t, onClose }: Props): JSX.Element => {
|
|
20
|
+
const [ copied, setCopied ] = useState<boolean>(false);
|
|
21
|
+
|
|
22
|
+
const onCopy = (): void => {
|
|
23
|
+
navigator.clipboard.writeText(token).then(() => {
|
|
24
|
+
setCopied(true);
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Modal
|
|
30
|
+
width={ 600 }
|
|
31
|
+
title={ t('admin_api_consumers.token.reveal.title', { ns: 'common' }) }
|
|
32
|
+
content={
|
|
33
|
+
<>
|
|
34
|
+
<p>{ t('admin_api_consumers.token.reveal.notice', { ns: 'common' }) }</p>
|
|
35
|
+
|
|
36
|
+
<TokenBox>
|
|
37
|
+
<TokenValue>{ token }</TokenValue>
|
|
38
|
+
|
|
39
|
+
<Button
|
|
40
|
+
type="button"
|
|
41
|
+
size="small"
|
|
42
|
+
subtype="secondary"
|
|
43
|
+
noMargin
|
|
44
|
+
text={
|
|
45
|
+
<>
|
|
46
|
+
{ copied ? <FiCheck /> : <FiCopy /> }
|
|
47
|
+
{ t(copied ? 'admin_api_consumers.token.reveal.copied' : 'admin_api_consumers.token.reveal.copy', { ns: 'common' }) }
|
|
48
|
+
</>
|
|
49
|
+
}
|
|
50
|
+
onClick={ onCopy }
|
|
51
|
+
/>
|
|
52
|
+
</TokenBox>
|
|
53
|
+
|
|
54
|
+
<Button
|
|
55
|
+
type="button"
|
|
56
|
+
size="medium"
|
|
57
|
+
text={ t('admin_api_consumers.token.reveal.done', { ns: 'common' }) }
|
|
58
|
+
onClick={ onClose }
|
|
59
|
+
/>
|
|
60
|
+
</>
|
|
61
|
+
}
|
|
62
|
+
onClose={ onClose }
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default TokenRevealModal;
|
package/index.ts
ADDED
package/package.json
ADDED
package/services.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { UseMutationResult, UseQueryResult, UseSuspenseQueryResult, useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
|
2
|
+
import { apiClient } from '@autobusal/providers';
|
|
3
|
+
import { PaginationData } from '@autobusal/providers/types/pagination';
|
|
4
|
+
import { TableParamsData } from '@autobusal/providers/types/other';
|
|
5
|
+
import { DropdownData } from '@autobusal/providers/types/other';
|
|
6
|
+
import { ApiConsumerData } from './types';
|
|
7
|
+
|
|
8
|
+
export const useGetApiConsumers = (params: TableParamsData): UseQueryResult<PaginationData<ApiConsumerData>> => (
|
|
9
|
+
useQuery({
|
|
10
|
+
queryKey: ['admin-api-consumers', params],
|
|
11
|
+
queryFn: async () => (
|
|
12
|
+
await apiClient
|
|
13
|
+
.get('/api/api_consumers/admin/browse', { params })
|
|
14
|
+
.then(response => (
|
|
15
|
+
response.data
|
|
16
|
+
))
|
|
17
|
+
)
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export const useGetApiConsumer = (id: number): UseSuspenseQueryResult<ApiConsumerData | null> => (
|
|
22
|
+
useSuspenseQuery({
|
|
23
|
+
queryKey: ['admin-api-consumers-manage', { id }],
|
|
24
|
+
queryFn: async () => {
|
|
25
|
+
if (id === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return await apiClient
|
|
30
|
+
.get('/api/api_consumers/admin/get', {
|
|
31
|
+
params: { id }
|
|
32
|
+
})
|
|
33
|
+
.then(response => (
|
|
34
|
+
response.data
|
|
35
|
+
));
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// the source for the external_providers allowlist checkbox list - every
|
|
41
|
+
// provider slug the app knows how to talk to (Libraries\External\Registry::available())
|
|
42
|
+
export const useGetProviders = (): UseQueryResult<DropdownData[]> => (
|
|
43
|
+
useQuery({
|
|
44
|
+
queryKey: ['admin-api-consumers-providers'],
|
|
45
|
+
queryFn: async () => (
|
|
46
|
+
await apiClient
|
|
47
|
+
.get('/api/external/admin/providers')
|
|
48
|
+
.then(response => (
|
|
49
|
+
response.data.map((item: { id: string, name: string }) => ({ id: item.id, name: item.name }))
|
|
50
|
+
))
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-27
|
|
56
|
+
// The backend returns `api_token`, not `token` - magus's BFF proxy
|
|
57
|
+
// (public/bff/index.php) treats ANY top-level `token` key on a
|
|
58
|
+
// /bff/api/... response as a fresh session auth token to store server-side
|
|
59
|
+
// and strip from the response, with no notion of whose token it is. A
|
|
60
|
+
// plain `token` here would silently hijack the ADMIN'S OWN session onto
|
|
61
|
+
// the newly-created API consumer's identity (confirmed live during
|
|
62
|
+
// testing). Do not rename this back to `token` without fixing the BFF.
|
|
63
|
+
export const usePostApiConsumer = (id: number): UseMutationResult<{ api_token: string | null }, Error, Record<string, any>, unknown> => (
|
|
64
|
+
useMutation({
|
|
65
|
+
mutationKey: ['admin-api-consumers-save', { id }],
|
|
66
|
+
mutationFn: async (data: Record<string, any>) => {
|
|
67
|
+
const formData = new FormData();
|
|
68
|
+
|
|
69
|
+
if (id > 0) {
|
|
70
|
+
formData.append('id', String(id));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Object.keys(data).map(name => {
|
|
74
|
+
if (name === 'operators' || name === 'external_providers') {
|
|
75
|
+
formData.append(name, JSON.stringify(data[name]));
|
|
76
|
+
} else if (data[name] !== undefined && data[name] !== null) {
|
|
77
|
+
formData.append(name, String(data[name]));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return await apiClient
|
|
82
|
+
.post('/api/api_consumers/admin/update', formData, {
|
|
83
|
+
headers: {
|
|
84
|
+
'Content-Type': 'multipart/form-data'
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
.then(response => (
|
|
88
|
+
response.data
|
|
89
|
+
));
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
export const useDeleteApiConsumer = (): UseMutationResult<void, Error, number, unknown> => (
|
|
95
|
+
useMutation({
|
|
96
|
+
mutationKey: ['admin-api-consumers-delete'],
|
|
97
|
+
mutationFn: async (id: number) => (
|
|
98
|
+
await apiClient
|
|
99
|
+
.delete('/api/api_consumers/admin/delete', {
|
|
100
|
+
params: { id }
|
|
101
|
+
})
|
|
102
|
+
.then(response => (
|
|
103
|
+
response.data
|
|
104
|
+
))
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
export const useToggleApiConsumer = (): UseMutationResult<void, Error, { id: number, api: number }, unknown> => (
|
|
110
|
+
useMutation({
|
|
111
|
+
mutationKey: ['admin-api-consumers-toggle'],
|
|
112
|
+
mutationFn: async ({ id, api }: { id: number, api: number }) => (
|
|
113
|
+
await apiClient
|
|
114
|
+
.post('/api/api_consumers/admin/toggle', { id, api })
|
|
115
|
+
.then(response => (
|
|
116
|
+
response.data
|
|
117
|
+
))
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
export const useRevokeApiConsumerToken = (): UseMutationResult<void, Error, number, unknown> => (
|
|
123
|
+
useMutation({
|
|
124
|
+
mutationKey: ['admin-api-consumers-revoke'],
|
|
125
|
+
mutationFn: async (id: number) => (
|
|
126
|
+
await apiClient
|
|
127
|
+
.post('/api/api_consumers/admin/revoke', { id })
|
|
128
|
+
.then(response => (
|
|
129
|
+
response.data
|
|
130
|
+
))
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
export const useRegenerateApiConsumerToken = (): UseMutationResult<{ api_token: string }, Error, number, unknown> => (
|
|
136
|
+
useMutation({
|
|
137
|
+
mutationKey: ['admin-api-consumers-regenerate'],
|
|
138
|
+
mutationFn: async (id: number) => (
|
|
139
|
+
await apiClient
|
|
140
|
+
.post('/api/api_consumers/admin/regenerate', { id })
|
|
141
|
+
.then(response => (
|
|
142
|
+
response.data
|
|
143
|
+
))
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
);
|
package/styles.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const TokenBox = styled.div`
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
gap: 10px;
|
|
7
|
+
margin: 15px 0;
|
|
8
|
+
padding: 12px 15px;
|
|
9
|
+
background: ${ props => props.theme.primary.neutral };
|
|
10
|
+
border-radius: 8px;
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
export const TokenValue = styled.code`
|
|
14
|
+
flex: 1;
|
|
15
|
+
font-family: monospace;
|
|
16
|
+
font-size: ${ props => props.theme.size.sm };
|
|
17
|
+
word-break: break-all;
|
|
18
|
+
color: ${ props => props.theme.font.normal };
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
export const TokenStatus = styled.div`
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: 10px;
|
|
25
|
+
margin-bottom: 10px;
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export const TokenActions = styled.div`
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: 10px;
|
|
31
|
+
`;
|
package/types.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ComissionData } from '@autobusal/providers/types/users';
|
|
2
|
+
|
|
3
|
+
export interface ApiConsumerData {
|
|
4
|
+
id: number
|
|
5
|
+
company: string
|
|
6
|
+
operators: (number | string)[]
|
|
7
|
+
external_providers: (number | string)[]
|
|
8
|
+
is_api_consumer: boolean
|
|
9
|
+
user: {
|
|
10
|
+
id: number
|
|
11
|
+
email: string
|
|
12
|
+
status: number
|
|
13
|
+
comissions?: ComissionData
|
|
14
|
+
api?: {
|
|
15
|
+
api: number
|
|
16
|
+
has_token: boolean
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|