diff --git a/packages/app-builder/public/locales/en/common.json b/packages/app-builder/public/locales/en/common.json
index a9a5cc4f0..7dc960523 100644
--- a/packages/app-builder/public/locales/en/common.json
+++ b/packages/app-builder/public/locales/en/common.json
@@ -23,7 +23,7 @@
"delete": "Delete",
"close": "Close",
"clipboard.aria-label": "Copy to clipboard: {{value}}",
- "clipboard.copy": "Copied in clipboard: {{value}}",
+ "clipboard.copy": "Copied in clipboard: {{value}} ",
"empty_scenario_iteration_list": "You don't have any rules. Click on create rule to make a new one",
"success.save": "Saved successfully",
"success.add_to_case": "Decision successfully added to case",
diff --git a/packages/app-builder/public/locales/en/settings.json b/packages/app-builder/public/locales/en/settings.json
index 46ab3300b..9c3bef720 100644
--- a/packages/app-builder/public/locales/en/settings.json
+++ b/packages/app-builder/public/locales/en/settings.json
@@ -23,6 +23,15 @@
"users.inbox_user_role.member_count_other": "MEMBER in {{count}} inboxes",
"users.inbox_user_role.unknown": "unknown",
"api_keys": "API Keys",
+ "api_keys.new_api_key": "New API Key",
+ "api_keys.copy_api_key": "Make sure to copy it now and store it in a secure place as you will not be able to see this again.",
+ "api_keys.description": "Description",
+ "api_keys.role": "API key role",
+ "api_keys.role.api_client": "API_CLIENT",
+ "api_keys.role.unknown": "UNKNOWN",
+ "api_keys.create": "Create new API Key",
+ "api_keys.delete": "Delete API Key",
+ "api_keys.delete.content": "You are about to revoke this API key. This action is irreversible, do you want to proceed?",
"case_manager": "Case Manager",
"inboxes": "Inboxes",
"inboxes.new_inbox": "New inbox",
diff --git a/packages/app-builder/src/components/CopyToClipboardButton.tsx b/packages/app-builder/src/components/CopyToClipboardButton.tsx
index 193486d6e..f2ae451e0 100644
--- a/packages/app-builder/src/components/CopyToClipboardButton.tsx
+++ b/packages/app-builder/src/components/CopyToClipboardButton.tsx
@@ -12,7 +12,7 @@ export const CopyToClipboardButton = forwardRef<
return (
diff --git a/packages/app-builder/src/models/api-keys.ts b/packages/app-builder/src/models/api-keys.ts
new file mode 100644
index 000000000..f2619b032
--- /dev/null
+++ b/packages/app-builder/src/models/api-keys.ts
@@ -0,0 +1,43 @@
+import { type ApiKeyDto, type CreatedApiKeyDto } from 'marble-api';
+import { assertNever } from 'typescript-utils';
+
+export const apiKeyRoleOptions = ['API_CLIENT'] as const;
+type ApiKeyRole = (typeof apiKeyRoleOptions)[number];
+
+function isApiKeyRole(role: string): role is ApiKeyRole {
+ return apiKeyRoleOptions.includes(role as ApiKeyRole);
+}
+
+export interface ApiKey {
+ id: string;
+ organizationId: string;
+ description: string;
+ role: ApiKeyRole | 'UNKNWON';
+}
+
+export function adaptApiKey(apiKeyDto: ApiKeyDto): ApiKey {
+ const apiKey: ApiKey = {
+ id: apiKeyDto.id,
+ organizationId: apiKeyDto.organization_id,
+ description: apiKeyDto.description,
+ role: 'UNKNWON',
+ };
+ if (isApiKeyRole(apiKeyDto.role)) {
+ apiKey.role = apiKeyDto.role;
+ } else {
+ // @ts-expect-error should be unreachable if all roles are handled
+ assertNever('[ApiKeyDto] Unknown role', apiKeyDto.role);
+ }
+ return apiKey;
+}
+
+export type CreatedApiKey = ApiKey & {
+ key: string;
+};
+
+export function adaptCreatedApiKey(apiKey: CreatedApiKeyDto): CreatedApiKey {
+ return {
+ ...adaptApiKey(apiKey),
+ key: apiKey.key,
+ };
+}
diff --git a/packages/app-builder/src/models/marble-session.ts b/packages/app-builder/src/models/marble-session.ts
index bfef0bb67..ea0131e6e 100644
--- a/packages/app-builder/src/models/marble-session.ts
+++ b/packages/app-builder/src/models/marble-session.ts
@@ -1,11 +1,13 @@
import { type Session } from '@remix-run/node';
import { type Token } from 'marble-api';
+import { type CreatedApiKey } from './api-keys';
import { type AuthErrors } from './auth-errors';
import { type CurrentUser } from './user';
export type AuthData = { authToken: Token; lng: string; user: CurrentUser };
export type AuthFlashData = {
authError: { message: AuthErrors };
+ createdApiKey: CreatedApiKey;
};
export type AuthSession = Session
;
diff --git a/packages/app-builder/src/repositories/ApiKeyRepository.ts b/packages/app-builder/src/repositories/ApiKeyRepository.ts
new file mode 100644
index 000000000..a29f91f85
--- /dev/null
+++ b/packages/app-builder/src/repositories/ApiKeyRepository.ts
@@ -0,0 +1,37 @@
+import { type MarbleApi } from '@app-builder/infra/marble-api';
+import {
+ adaptApiKey,
+ adaptCreatedApiKey,
+ type ApiKey,
+ type CreatedApiKey,
+} from '@app-builder/models/api-keys';
+
+export interface ApiKeyRepository {
+ listApiKeys(): Promise;
+ createApiKey(args: {
+ description: string;
+ role: string;
+ }): Promise;
+ deleteApiKey(args: { apiKeyId: string }): Promise;
+}
+
+export function getApiKeyRepository() {
+ return (marbleApiClient: MarbleApi): ApiKeyRepository => ({
+ listApiKeys: async () => {
+ const { api_keys } = await marbleApiClient.listApiKeys();
+
+ return api_keys.map(adaptApiKey);
+ },
+ createApiKey: async ({ description, role }) => {
+ const { api_key } = await marbleApiClient.createApiKey({
+ description,
+ role,
+ });
+
+ return adaptCreatedApiKey(api_key);
+ },
+ deleteApiKey: async ({ apiKeyId }) => {
+ await marbleApiClient.deleteApiKey(apiKeyId);
+ },
+ });
+}
diff --git a/packages/app-builder/src/repositories/init.server.ts b/packages/app-builder/src/repositories/init.server.ts
index ef5d5b0fb..04216d368 100644
--- a/packages/app-builder/src/repositories/init.server.ts
+++ b/packages/app-builder/src/repositories/init.server.ts
@@ -1,5 +1,6 @@
import { type GetMarbleAPIClient } from '@app-builder/infra/marble-api';
+import { getApiKeyRepository } from './ApiKeyRepository';
import { getCaseRepository } from './CaseRepository';
import { getDataModelRepository } from './DataModelRepository';
import { getDecisionRepository } from './DecisionRepository';
@@ -40,6 +41,7 @@ export function makeServerRepositories({
scenarioRepository: getScenarioRepository(),
organizationRepository: getOrganizationRepository(),
dataModelRepository: getDataModelRepository(),
+ apiKeyRepository: getApiKeyRepository(),
};
}
diff --git a/packages/app-builder/src/routes/_builder+/settings+/_layout.tsx b/packages/app-builder/src/routes/_builder+/settings+/_layout.tsx
index 3ab4f1b4a..86d95e8e0 100644
--- a/packages/app-builder/src/routes/_builder+/settings+/_layout.tsx
+++ b/packages/app-builder/src/routes/_builder+/settings+/_layout.tsx
@@ -39,12 +39,12 @@ export default function Settings() {
to={getRoute('/settings/users')}
/>
- {/*
-
- */}
+
+
+
();
+
export default function ApiKeys() {
- return ApiKeys
;
+ const { t } = useTranslation(['settings']);
+ const { apiKeys, createdApiKey } = useLoaderData();
+
+ const columns = useMemo(() => {
+ return [
+ columnHelper.accessor((row) => row.description, {
+ id: 'description',
+ header: t('settings:api_keys.description'),
+ size: 300,
+ }),
+ columnHelper.accessor((row) => row.role, {
+ id: 'role',
+ header: t('settings:api_keys.role'),
+ size: 150,
+ cell: ({ getValue }) => t(tKeyForApiKeyRole(getValue())),
+ }),
+ columnHelper.display({
+ id: 'actions',
+ size: 100,
+ cell: ({ cell }) => {
+ return (
+
+
+
+ );
+ },
+ }),
+ ];
+ }, [t]);
+
+ const { table, getBodyProps, rows, getContainerProps } = useTable({
+ data: apiKeys,
+ columns,
+ columnResizeMode: 'onChange',
+ getCoreRowModel: getCoreRowModel(),
+ });
+
+ return (
+
+
+ {createdApiKey ? : null}
+
+
+ {t('settings:tags')}
+
+
+
+
+
+
+ {rows.map((row) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+ );
+}
+
+function CreatedAPIKey({ createdApiKey }: { createdApiKey: CreatedApiKey }) {
+ const { t } = useTranslation(['settings']);
+ return (
+
+
+ {t('settings:api_keys.new_api_key')}
+ {t('settings:api_keys.copy_api_key')}
+
+ {createdApiKey.key}
+
+
+
+ );
}
diff --git a/packages/app-builder/src/routes/ressources+/auth+/refresh.tsx b/packages/app-builder/src/routes/ressources+/auth+/refresh.tsx
index 1c3a1366e..284a66f60 100644
--- a/packages/app-builder/src/routes/ressources+/auth+/refresh.tsx
+++ b/packages/app-builder/src/routes/ressources+/auth+/refresh.tsx
@@ -34,7 +34,7 @@ export function useRefreshToken() {
.authenticationClientRepository;
firebaseIdToken().then(
- (idToken) => {
+ (idToken: string) => {
submit(
{ idToken, csrf },
{ method: 'POST', action: getRoute('/ressources/auth/refresh') },
diff --git a/packages/app-builder/src/routes/ressources+/settings+/api-keys+/create.tsx b/packages/app-builder/src/routes/ressources+/settings+/api-keys+/create.tsx
new file mode 100644
index 000000000..5f3b6ab4c
--- /dev/null
+++ b/packages/app-builder/src/routes/ressources+/settings+/api-keys+/create.tsx
@@ -0,0 +1,156 @@
+import { FormError } from '@app-builder/components/Form/FormError';
+import { FormField } from '@app-builder/components/Form/FormField';
+import { FormInput } from '@app-builder/components/Form/FormInput';
+import { FormLabel } from '@app-builder/components/Form/FormLabel';
+import { FormSelect } from '@app-builder/components/Form/FormSelect';
+import { setToastMessage } from '@app-builder/components/MarbleToaster';
+import { apiKeyRoleOptions } from '@app-builder/models/api-keys';
+import { tKeyForApiKeyRole } from '@app-builder/services/i18n/translation-keys/api-key';
+import { serverServices } from '@app-builder/services/init.server';
+import { getRoute } from '@app-builder/utils/routes';
+import { useForm } from '@conform-to/react';
+import { getFieldsetConstraint, parse } from '@conform-to/zod';
+import { type ActionFunctionArgs, json, redirect } from '@remix-run/node';
+import { useFetcher, useNavigation } from '@remix-run/react';
+import { useEffect, useId, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { AuthenticityTokenInput } from 'remix-utils/csrf/react';
+import { Button, Modal } from 'ui-design-system';
+import { Icon } from 'ui-icons';
+import { z } from 'zod';
+
+const createApiKeyFormSchema = z.object({
+ description: z.string().min(1),
+ role: z.enum(apiKeyRoleOptions),
+});
+
+export async function action({ request }: ActionFunctionArgs) {
+ const { authService, csrfService, toastSessionService, authSessionService } =
+ serverServices;
+ const { apiKey } = await authService.isAuthenticated(request, {
+ failureRedirect: getRoute('/sign-in'),
+ });
+ await csrfService.validate(request);
+
+ const formData = await request.formData();
+ const submission = parse(formData, { schema: createApiKeyFormSchema });
+
+ if (submission.intent !== 'submit' || !submission.value) {
+ return json(submission);
+ }
+
+ try {
+ const createdApiKey = await apiKey.createApiKey(submission.value);
+
+ const authSession = await authSessionService.getSession(request);
+ authSession.flash('createdApiKey', createdApiKey);
+
+ return redirect(getRoute('/settings/api-keys'), {
+ headers: {
+ 'Set-Cookie': await authSessionService.commitSession(authSession),
+ },
+ });
+ } catch (error) {
+ const toastSession = await toastSessionService.getSession(request);
+ setToastMessage(toastSession, {
+ type: 'error',
+ messageKey: 'common:errors.unknown',
+ });
+ return json(submission, {
+ headers: {
+ 'Set-Cookie': await toastSessionService.commitSession(toastSession),
+ },
+ });
+ }
+}
+
+export function CreateApiKey() {
+ const { t } = useTranslation(['settings']);
+ const [open, setOpen] = useState(false);
+
+ const navigation = useNavigation();
+ useEffect(() => {
+ if (navigation.state === 'loading') {
+ setOpen(false);
+ }
+ }, [navigation.state]);
+
+ return (
+
+ e.stopPropagation()} asChild>
+ e.stopPropagation()}>
+
+ {t('settings:api_keys.new_api_key')}
+
+
+ e.stopPropagation()}>
+
+
+
+ );
+}
+
+const CreateApiKeyContent = () => {
+ const { t } = useTranslation(['settings', 'common']);
+ const fetcher = useFetcher();
+
+ const formId = useId();
+ const [form, { description, role }] = useForm({
+ id: formId,
+ defaultValue: { description: '', role: 'API_CLIENT' },
+ lastSubmission: fetcher.data,
+ constraint: getFieldsetConstraint(createApiKeyFormSchema),
+ onValidate({ formData }) {
+ return parse(formData, {
+ schema: createApiKeyFormSchema,
+ });
+ },
+ });
+
+ return (
+
+ {t('settings:api_keys.new_api_key')}
+
+
+
+ {t('settings:api_keys.description')}
+
+
+
+
+ {t('settings:api_keys.role')}
+
+ {apiKeyRoleOptions.map((role) => (
+
+ {t(tKeyForApiKeyRole(role))}
+
+ ))}
+
+
+
+
+
+
+ {t('common:cancel')}
+
+
+
+ {t('settings:api_keys.create')}
+
+
+
+
+ );
+};
diff --git a/packages/app-builder/src/routes/ressources+/settings+/api-keys+/delete.tsx b/packages/app-builder/src/routes/ressources+/settings+/api-keys+/delete.tsx
new file mode 100644
index 000000000..278bdc834
--- /dev/null
+++ b/packages/app-builder/src/routes/ressources+/settings+/api-keys+/delete.tsx
@@ -0,0 +1,91 @@
+import { type ApiKey } from '@app-builder/models/api-keys';
+import { serverServices } from '@app-builder/services/init.server';
+import { parseForm } from '@app-builder/utils/input-validation';
+import { getRoute } from '@app-builder/utils/routes';
+import { type ActionFunctionArgs, redirect } from '@remix-run/node';
+import { Form, useNavigation } from '@remix-run/react';
+import { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Button, Modal } from 'ui-design-system';
+import { Icon } from 'ui-icons';
+import { z } from 'zod';
+
+const deleteApiKeyFormSchema = z.object({
+ apiKeyId: z.string().uuid(),
+});
+
+export async function action({ request }: ActionFunctionArgs) {
+ const { authService } = serverServices;
+ const { apiKey } = await authService.isAuthenticated(request, {
+ failureRedirect: getRoute('/sign-in'),
+ });
+
+ const formData = await parseForm(request, deleteApiKeyFormSchema);
+
+ await apiKey.deleteApiKey(formData);
+ return redirect(getRoute('/settings/api-keys'));
+}
+
+export function DeleteApiKey({ apiKey }: { apiKey: ApiKey }) {
+ const { t } = useTranslation(['settings']);
+
+ const [open, setOpen] = useState(false);
+
+ const navigation = useNavigation();
+ useEffect(() => {
+ if (navigation.state === 'loading') {
+ setOpen(false);
+ }
+ }, [navigation.state]);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+function DeleteApiKeyContent({ apiKey }: { apiKey: ApiKey }) {
+ const { t } = useTranslation(['settings', 'common']);
+
+ return (
+
+ );
+}
diff --git a/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.create.tsx b/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.create.tsx
index cf88a6eef..0e3f19b7b 100644
--- a/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.create.tsx
+++ b/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.create.tsx
@@ -15,7 +15,7 @@ import { useFetcher, useNavigation } from '@remix-run/react';
import { type Namespace } from 'i18next';
import { useEffect, useId, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { Button, Modal, Select } from 'ui-design-system';
+import { Button, Modal } from 'ui-design-system';
import { Icon } from 'ui-icons';
import { z } from 'zod';
@@ -136,10 +136,7 @@ export function CreateInboxUserContent({
-
+
{t('settings:inboxes.inbox_details.user')}
{userOptions.map(({ id, name }) => (
@@ -154,9 +151,9 @@ export function CreateInboxUserContent({
{t('settings:inboxes.inbox_details.role')}
{roleOptions.map((role) => (
-
+
{t(tKeyForInboxUserRole(role))}
-
+
))}
diff --git a/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.update.tsx b/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.update.tsx
index 27f45a76f..943cf8a9e 100644
--- a/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.update.tsx
+++ b/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.update.tsx
@@ -129,10 +129,7 @@ export function UpdateInboxUserContent({
-
+
{t('settings:inboxes.inbox_details.role')}
{roleOptions.map((role) => (
diff --git a/packages/app-builder/src/routes/ressources+/settings+/tags+/create.tsx b/packages/app-builder/src/routes/ressources+/settings+/tags+/create.tsx
index fba54de9e..7fdb39b67 100644
--- a/packages/app-builder/src/routes/ressources+/settings+/tags+/create.tsx
+++ b/packages/app-builder/src/routes/ressources+/settings+/tags+/create.tsx
@@ -116,7 +116,7 @@ const CreateTagContent = () => {
>
{t('settings:tags.new_tag')}
-
+
{t('settings:tags.name')}
diff --git a/packages/app-builder/src/routes/ressources+/settings+/tags+/update.tsx b/packages/app-builder/src/routes/ressources+/settings+/tags+/update.tsx
index 07ca7de85..b71071ec1 100644
--- a/packages/app-builder/src/routes/ressources+/settings+/tags+/update.tsx
+++ b/packages/app-builder/src/routes/ressources+/settings+/tags+/update.tsx
@@ -113,7 +113,7 @@ const UpdateTagContent = ({ tag }: { tag: Tag }) => {
>
{t('settings:tags.new_tag')}
-
+
{t('settings:tags.name')}
diff --git a/packages/app-builder/src/routes/ressources+/settings+/users+/create.tsx b/packages/app-builder/src/routes/ressources+/settings+/users+/create.tsx
index 3b84e58b9..bb4380e00 100644
--- a/packages/app-builder/src/routes/ressources+/settings+/users+/create.tsx
+++ b/packages/app-builder/src/routes/ressources+/settings+/users+/create.tsx
@@ -135,7 +135,7 @@ const CreateUserContent = ({ orgId }: { orgId: string }) => {
>
{t('settings:users.new_user')}
-
+
diff --git a/packages/app-builder/src/routes/ressources+/settings+/users+/update.tsx b/packages/app-builder/src/routes/ressources+/settings+/users+/update.tsx
index 684af75aa..5069e17e6 100644
--- a/packages/app-builder/src/routes/ressources+/settings+/users+/update.tsx
+++ b/packages/app-builder/src/routes/ressources+/settings+/users+/update.tsx
@@ -111,7 +111,7 @@ const UpdateUserContent = ({ user }: { user: User }) => {
>
{t('settings:users.update_user')}
-
+
diff --git a/packages/app-builder/src/services/auth/auth.server.ts b/packages/app-builder/src/services/auth/auth.server.ts
index b89d9b8c6..7e4748815 100644
--- a/packages/app-builder/src/services/auth/auth.server.ts
+++ b/packages/app-builder/src/services/auth/auth.server.ts
@@ -5,6 +5,7 @@ import {
type AuthFlashData,
type CurrentUser,
} from '@app-builder/models';
+import { type ApiKeyRepository } from '@app-builder/repositories/ApiKeyRepository';
import { type CaseRepository } from '@app-builder/repositories/CaseRepository';
import { type DataModelRepository } from '@app-builder/repositories/DataModelRepository';
import { type DecisionRepository } from '@app-builder/repositories/DecisionRepository';
@@ -31,6 +32,7 @@ interface AuthenticatedInfo {
decision: DecisionRepository;
cases: CaseRepository;
dataModelRepository: DataModelRepository;
+ apiKey: ApiKeyRepository;
organization: OrganizationRepository;
scenario: ScenarioRepository;
user: CurrentUser;
@@ -91,6 +93,7 @@ export function makeAuthenticationServerService(
) => OrganizationRepository,
scenarioRepository: (marbleApiClient: MarbleApi) => ScenarioRepository,
dataModelRepository: (marbleApiClient: MarbleApi) => DataModelRepository,
+ apiKeysRepository: (marbleApiClient: MarbleApi) => ApiKeyRepository,
authSessionService: SessionService
,
csrfService: CSRF,
) {
@@ -246,6 +249,7 @@ export function makeAuthenticationServerService(
scenario: scenarioRepository(apiClient),
organization: organizationRepository(apiClient, user.organizationId),
dataModelRepository: dataModelRepository(apiClient),
+ apiKey: apiKeysRepository(apiClient),
user,
inbox: inboxRepository(apiClient),
};
diff --git a/packages/app-builder/src/services/i18n/translation-keys/api-key.ts b/packages/app-builder/src/services/i18n/translation-keys/api-key.ts
new file mode 100644
index 000000000..80c33cafb
--- /dev/null
+++ b/packages/app-builder/src/services/i18n/translation-keys/api-key.ts
@@ -0,0 +1,10 @@
+import { type ApiKey } from '@app-builder/models/api-keys';
+
+export function tKeyForApiKeyRole(role: ApiKey['role']) {
+ switch (role) {
+ case 'API_CLIENT':
+ return 'settings:api_keys.role.api_client';
+ default:
+ return 'settings:api_keys.role.unknown';
+ }
+}
diff --git a/packages/app-builder/src/services/init.server.ts b/packages/app-builder/src/services/init.server.ts
index f22f11a9d..f4577f4b5 100644
--- a/packages/app-builder/src/services/init.server.ts
+++ b/packages/app-builder/src/services/init.server.ts
@@ -36,6 +36,7 @@ function makeServerServices(repositories: ServerRepositories) {
repositories.organizationRepository,
repositories.scenarioRepository,
repositories.dataModelRepository,
+ repositories.apiKeyRepository,
authSessionService,
csrfService,
),
diff --git a/packages/app-builder/src/utils/routes/routes.ts b/packages/app-builder/src/utils/routes/routes.ts
index dca817ef1..9378218e7 100644
--- a/packages/app-builder/src/utils/routes/routes.ts
+++ b/packages/app-builder/src/utils/routes/routes.ts
@@ -350,6 +350,16 @@ export const routes = [
"path": "ressources/scenarios/update",
"file": "routes/ressources+/scenarios+/update.tsx"
},
+ {
+ "id": "routes/ressources+/settings+/api-keys+/create",
+ "path": "ressources/settings/api-keys/create",
+ "file": "routes/ressources+/settings+/api-keys+/create.tsx"
+ },
+ {
+ "id": "routes/ressources+/settings+/api-keys+/delete",
+ "path": "ressources/settings/api-keys/delete",
+ "file": "routes/ressources+/settings+/api-keys+/delete.tsx"
+ },
{
"id": "routes/ressources+/settings+/inboxes+/create",
"path": "ressources/settings/inboxes/create",
diff --git a/packages/app-builder/src/utils/routes/types.ts b/packages/app-builder/src/utils/routes/types.ts
index 063a540f1..cdfaf90d5 100644
--- a/packages/app-builder/src/utils/routes/types.ts
+++ b/packages/app-builder/src/utils/routes/types.ts
@@ -59,6 +59,8 @@ export type RoutePath =
| '/ressources/scenarios/create'
| '/ressources/scenarios/deployment'
| '/ressources/scenarios/update'
+ | '/ressources/settings/api-keys/create'
+ | '/ressources/settings/api-keys/delete'
| '/ressources/settings/inboxes/create'
| '/ressources/settings/inboxes/delete'
| '/ressources/settings/inboxes/inbox-users/create'
@@ -141,6 +143,8 @@ export type RouteID =
| 'routes/ressources+/scenarios+/create'
| 'routes/ressources+/scenarios+/deployment'
| 'routes/ressources+/scenarios+/update'
+ | 'routes/ressources+/settings+/api-keys+/create'
+ | 'routes/ressources+/settings+/api-keys+/delete'
| 'routes/ressources+/settings+/inboxes+/create'
| 'routes/ressources+/settings+/inboxes+/delete'
| 'routes/ressources+/settings+/inboxes+/inbox-users.create'
diff --git a/packages/app-builder/src/utils/use-get-copy-to-clipboard.ts b/packages/app-builder/src/utils/use-get-copy-to-clipboard.ts
deleted file mode 100644
index 1125c52f3..000000000
--- a/packages/app-builder/src/utils/use-get-copy-to-clipboard.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import toast from 'react-hot-toast';
-import { useTranslation } from 'react-i18next';
-
-export function useGetCopyToClipboard() {
- const { t } = useTranslation('common');
- return (value: string) => ({
- type: 'button',
- 'aria-label': t('clipboard.aria-label', {
- replace: {
- value,
- },
- }),
- onClick: async () => {
- try {
- await navigator.clipboard.writeText(value);
- toast.success(
- t('clipboard.copy', {
- replace: {
- value,
- },
- }),
- );
- } catch (err) {
- toast.error(t('errors.unknown'));
- }
- },
- });
-}
diff --git a/packages/app-builder/src/utils/use-get-copy-to-clipboard.tsx b/packages/app-builder/src/utils/use-get-copy-to-clipboard.tsx
new file mode 100644
index 000000000..4d3439938
--- /dev/null
+++ b/packages/app-builder/src/utils/use-get-copy-to-clipboard.tsx
@@ -0,0 +1,37 @@
+import toast from 'react-hot-toast';
+import { Trans, useTranslation } from 'react-i18next';
+
+export function useGetCopyToClipboard() {
+ const { t } = useTranslation('common');
+ return (value: string) => ({
+ type: 'button',
+ 'aria-label': t('clipboard.aria-label', {
+ replace: {
+ value,
+ },
+ }),
+ onClick: async () => {
+ try {
+ await navigator.clipboard.writeText(value);
+ toast.success(() => (
+
+
+ ),
+ }}
+ values={{
+ value,
+ }}
+ />
+
+ ));
+ } catch (err) {
+ toast.error(t('errors.unknown'));
+ }
+ },
+ });
+}
diff --git a/packages/marble-api/scripts/openapi.yaml b/packages/marble-api/scripts/openapi.yaml
index 18278d4d3..fc6f62582 100644
--- a/packages/marble-api/scripts/openapi.yaml
+++ b/packages/marble-api/scripts/openapi.yaml
@@ -1892,7 +1892,64 @@ paths:
api_keys:
type: array
items:
- $ref: '#/components/schemas/ApiKey'
+ $ref: '#/components/schemas/ApiKeyDto'
+ '401':
+ $ref: '#/components/responses/401'
+ '403':
+ $ref: '#/components/responses/403'
+ '404':
+ $ref: '#/components/responses/404'
+ post:
+ tags:
+ - Authorization
+ summary: Create an api key
+ operationId: createApiKey
+ security:
+ - bearerAuth: []
+ requestBody:
+ description: 'Describe the api key to create'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateApiKey'
+ required: true
+ responses:
+ '200':
+ description: The created api key
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - api_key
+ properties:
+ api_key:
+ $ref: '#/components/schemas/CreatedApiKeyDto'
+ '401':
+ $ref: '#/components/responses/401'
+ '403':
+ $ref: '#/components/responses/403'
+ '404':
+ $ref: '#/components/responses/404'
+ /apikeys/{apiKeyId}:
+ delete:
+ tags:
+ - Authorization
+ summary: Delete an api key
+ operationId: deleteApiKey
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: apiKeyId
+ description: ID of the api key that need to be deleted
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ responses:
+ '204':
+ description: The api key has been deleted
'401':
$ref: '#/components/responses/401'
'403':
@@ -2838,23 +2895,42 @@ components:
type: string
last_name:
type: string
- ApiKey:
+ ApiKeyDto:
type: object
required:
- - api_key_id
+ - id
- organization_id
- - key
+ - description
- role
properties:
- api_key_id:
+ id:
type: string
organization_id:
type: string
format: uuid
- key:
+ description:
type: string
role:
type: string
+ CreateApiKey:
+ type: object
+ required:
+ - description
+ - role
+ properties:
+ description:
+ type: string
+ role:
+ type: string
+ CreatedApiKeyDto:
+ allOf:
+ - $ref: '#/components/schemas/ApiKeyDto'
+ - type: object
+ required:
+ - key
+ properties:
+ key:
+ type: string
LinkToSingleDto:
type: object
required:
diff --git a/packages/marble-api/src/generated/marble-api.ts b/packages/marble-api/src/generated/marble-api.ts
index ac238040b..a9cafe111 100644
--- a/packages/marble-api/src/generated/marble-api.ts
+++ b/packages/marble-api/src/generated/marble-api.ts
@@ -414,12 +414,19 @@ export type OpenApiSpec = {
securitySchemes?: object;
};
};
-export type ApiKey = {
- api_key_id: string;
+export type ApiKeyDto = {
+ id: string;
organization_id: string;
- key: string;
+ description: string;
+ role: string;
+};
+export type CreateApiKey = {
+ description: string;
role: string;
};
+export type CreatedApiKeyDto = ApiKeyDto & {
+ key: string;
+};
export type UserDto = {
user_id: string;
email: string;
@@ -1718,7 +1725,7 @@ export function listApiKeys(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: {
- api_keys: ApiKey[];
+ api_keys: ApiKeyDto[];
};
} | {
status: 401;
@@ -1733,6 +1740,50 @@ export function listApiKeys(opts?: Oazapfts.RequestOpts) {
...opts
}));
}
+/**
+ * Create an api key
+ */
+export function createApiKey(createApiKey: CreateApiKey, opts?: Oazapfts.RequestOpts) {
+ return oazapfts.ok(oazapfts.fetchJson<{
+ status: 200;
+ data: {
+ api_key: CreatedApiKeyDto;
+ };
+ } | {
+ status: 401;
+ data: string;
+ } | {
+ status: 403;
+ data: string;
+ } | {
+ status: 404;
+ data: string;
+ }>("/apikeys", oazapfts.json({
+ ...opts,
+ method: "POST",
+ body: createApiKey
+ })));
+}
+/**
+ * Delete an api key
+ */
+export function deleteApiKey(apiKeyId: string, opts?: Oazapfts.RequestOpts) {
+ return oazapfts.ok(oazapfts.fetchJson<{
+ status: 204;
+ } | {
+ status: 401;
+ data: string;
+ } | {
+ status: 403;
+ data: string;
+ } | {
+ status: 404;
+ data: string;
+ }>(`/apikeys/${encodeURIComponent(apiKeyId)}`, {
+ ...opts,
+ method: "DELETE"
+ }));
+}
/**
* List all users present in the database
*/